DataDog / datadog-react-native-wizard

Setup wizard for Datadog React Native SDK
Apache License 2.0
4 stars 2 forks source link

CodePush App with Custom iOS Build Config/Android flavors crashes immediately on startup after building post-wizard 🪄 #21

Open jstheoriginal opened 1 year ago

jstheoriginal commented 1 year ago

Describe what happened

When doing a release build, the app crashes immediately on startup. When not using the wizard, we don't have any issues launching the app.

Steps to reproduce the issue:

Run the wizard. Do a build to iOS. Open the app.

Expected behaviour:

No crash.

Actual behaviour:

Crash immediately on app launch.

Additional context

My guess is the app expects the js bundle file to be in a specific place but it's not there? That's the only thing I could see that this lib would do where it would cause a crash on startup.

We also use custom build configs:

It might also be the fact that we use codepush?

Additionally, when building a release build for Android, it just fails since it can't find the sourcemaps. they're there, but under the custom Android flavours (beta and prod). When looking in datadog-sourcemaps.gradle, it's not doing anything with flavour, so that's likely causing issues:

they're under this sourcemap path: /android/app/build/generated/sourcemaps/react/prodRelease

but it looks like it's trying to find it under: /android/app/build/generated/sourcemaps/react/release

louiszawadzki commented 1 year ago

Hi @jstheoriginal, thanks for reaching out!

Could you share the following information to help us troubleshoot:

Thanks a lot!

jstheoriginal commented 1 year ago

@louiszawadzki will do later today!

I also added more context around what I think the issue is in the original post, since I just tried Android and it fails to build at all. It's not taking flavor into account, so likely the iOS code is also only taking into account the default configs (Debug and Release), where we use custom ones for our beta app variants.

jstheoriginal commented 1 year ago

Putting this here for now regarding Android:

Once I took some code from the instabug sourcemap upload gradle file and hacked and slashed within the datadog sourcemaps file, I was able to get Android building successfully since it takes into account flavor:

/*
 * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
 * This product includes software developed at Datadog (https://www.datadoghq.com/).
 * Copyright 2016-Present Datadog, Inc.
 */

import org.apache.tools.ant.taskdefs.condition.Os

afterEvaluate {

    def androidExtension = extensions.findByName("android")

    if (androidExtension == null || android.applicationVariants == null) {
        throw new GradleException("Android extension is missing or running not in the application" +
                " module. Make sure you've applied datadog-sourcemaps.gradle in the" +
                " application module (usually it is located in the android/app/build.gradle file).")
    }

    gradle.projectsEvaluated {
        // Works for both `bundleReleaseJsAndAssets` and `createBundleReleaseJsAndAssets` and product flavors
        def suffix = 'ReleaseJsAndAssets'
        def bundleTasks = project(':app').tasks.findAll {
            task -> task.name.endsWith(suffix)
        }

        bundleTasks.forEach { task ->
            def name = task.name
            def prefixes = ['bundle', 'createBundle']
            def start = name.startsWith(prefixes[0]) ? prefixes[0].length() : prefixes[1].length()
            def end = name.length() - suffix.length()
            def flavor = name.substring(start, end).uncapitalize()

            task.finalizedBy createUploadSourcemapsTask(flavor)
        }
    }

    // androidExtension.applicationVariants.all { variant ->
    //     logger.info(variant.name + " " + variant.dirName + " " + variant.versionName + " " + variant.versionCode)
    //     def releaseVersion = variant.versionName
    //     def buildVersion = variant.versionCode

    //     if (releaseVersion == null) {
    //         throw new GradleException("Cannot determine application version. Make sure to" +
    //                 " define versionName property in the application/variant config.")
    //     }

    //     // locations and names are defined in "../../node_modules/react-native/react.gradle"
    //     def targetName = "ProdRelease" // variant.name.capitalize() // e.g. Release
    //     def targetPath = "prodRelease" // variant.dirName // e.g. release

    //     def reactConfig = getReactConfig(buildDir, targetName, targetPath)
    //     def reactRoot = file(reactConfig.root)

    //     def bundleTask = tasks.findByName(reactConfig.bundleTaskName)

    //     if (bundleTask == null) {
    //         logger.info("Cannot find JS bundle task for variant=${targetName}.")
    //         return
    //     }
    //     if (!bundleTask.enabled) {
    //         logger.info("JS bundle task for variant=${targetName} is not enabled.")
    //         return
    //     }

    //     def serviceName = getServiceName(variant)
    //     logger.info("Service name used for the upload of variant=${targetName} is ${serviceName}.")

    //     def bundleAssetName = reactConfig.bundleAssetName

    //     def jsSourceMapsDir = file("$buildDir/generated/sourcemaps/react/${targetPath}")
    //     def jsOutputSourceMapFile = file("$jsSourceMapsDir/${bundleAssetName}.map")

    //     def uploadTask = tasks.create("upload${targetName}Sourcemaps") {
    //         group = "datadog"
    //         description = "Uploads sourcemaps to Datadog."

    //         def execCommand = { jsBundleFile ->
    //             return [
    //                     "yarn", "datadog-ci", "react-native", "upload",
    //                     "--platform", "android",
    //                     "--service", serviceName,
    //                     "--bundle", jsBundleFile.absolutePath,
    //                     "--sourcemap", jsOutputSourceMapFile.absolutePath,
    //                     "--release-version", releaseVersion,
    //                     "--build-version", buildVersion
    //             ]
    //         }

    //         doFirst {
    //             def jsBundleFile = reactConfig.bundleFileResolver()
    //             if (jsBundleFile == null) {
    //                 throw new GradleException("JS bundle file doesn't exist, aborting upload.")
    //             }

    //             if (!jsOutputSourceMapFile.exists()) {
    //                 throw new GradleException("JS sourcemap file doesn't exist, aborting upload.")
    //             }

    //             runShellCommand(execCommand(jsBundleFile), reactRoot)

    //         }
    //     }

    //     uploadTask.dependsOn bundleTask
    //     bundleTask.finalizedBy uploadTask

    // }
}

Task createUploadSourcemapsTask(String flavor) {
    def name = 'uploadSourcemaps' + flavor.capitalize()

    // Don't recreate the task if it already exists.
    // This prevents the build from failing in an edge case where the user has
    // both `bundleReleaseJsAndAssets` and `createBundleReleaseJsAndAssets`
    def taskExists = tasks.getNames().contains(name)
    if (taskExists) {
        return tasks.named(name).get()
    }

    def provider = tasks.register(name) {
        group 'datadog'
        description 'Uploads sourcemaps file to Datadog server'
        // enabled = isUploadSourcemapsEnabled()

        doLast {
            try {
                def appProject = project(':app')
                def appDir = appProject.projectDir
                def flavorPath = flavor // + (flavor.empty ? '' : '/')
                def sourceMapDest = "build/generated/sourcemaps/react/${flavorPath}Release/index.android.bundle.map"
                def sourceMapFile = new File(appDir, sourceMapDest)
                def sourceDest = "build/generated/assets/createBundle${flavorPath}ReleaseJsAndAssets/index.android.bundle"
                def sourceFile = new File(appDir, sourceDest)

                if (!sourceMapFile.exists()) {
                    throw new InvalidUserDataException("Unable to find source map file at: ${sourceMapFile.absolutePath}")
                }

                def jsProjectDir = rootDir.parentFile
                // def instabugDir = new File(['node', '-p', 'require.resolve("instabug-reactnative/package.json")'].execute(null, rootDir).text.trim()).getParentFile()

                // def tokenShellFile = new File(instabugDir, 'scripts/find-token.sh')
                // def inferredToken = executeShellScript(tokenShellFile, jsProjectDir)
                // def appToken = resolveVar('App Token', 'INSTABUG_APP_TOKEN', inferredToken)

                def projectConfig = appProject.android.defaultConfig
                def versionName = resolveVar('Version Name', 'INSTABUG_VERSION_NAME', "${projectConfig.versionName}")
                def versionCode = resolveVar('Version Code', 'INSTABUG_VERSION_CODE', "${projectConfig.versionCode}")

                exec {
                    def osCompatibility = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c'] : []
                    def args = [
                            "yarn", "datadog-ci", "react-native", "upload",
                            "--platform", "android",
                            "--service", serviceName,
                            "--bundle", sourceFile.absolutePath,
                            "--sourcemap", sourceMapFile.absolutePath,
                            "--release-version", releaseVersion,
                            "--build-version", buildVersion
                    ]

                    commandLine(*osCompatibility, *args)
                }
            } catch (exception) {
                project.logger.error "Failed to upload source map file.\n" +
                        "Reason: ${exception.message}"
            }
        }
    }

    return provider.get()
}

/**
 * We use a function here to resolve the correct bundle location after the bundle task
 * is over, as its location can depend on the Android Gradle Plugin version.
 */
private def getBundleFileResolver(String[] jsBundleDirs, String bundleAssetName) {
    def resolver = { ->
        return jsBundleDirs.collect {
            file("$it/$bundleAssetName") 
        }.find {
            it.exists()
        }
    }

    return resolver
}

/** 
 * From RN 0.71, the following things changed in the RN gradle plugin:
 * - bundle task name changed from "bundle${targetName}JsAndAssets" to "createBundle${targetName}JsAndAssets"
 * - bundle dir changed from "$buildDir/generated/assets/react/${targetPath}" to "$buildDir/ASSETS/createBundle${targetName}JsAndAssets"
 *   (and then "$buildDir/generated/assets/createBundle${targetPath}JsAndAssets" with the release of Android Gradle Plugin 7.4, see https://github.com/facebook/react-native/issues/35439)
 * - config was in "project.react" and is now in "project.extensions.react"
 * - accessing parameters values requires calling a getter 
 */
private def getReactConfig(File buildDir, String targetName, String targetPath) {
    def reactConfig = [:]

    if (project.extensions.findByName("react")) {
        // From RN 0.71
        def bundleAssetName = project.extensions.react.bundleAssetName.get() ?: "index.android.bundle"
        reactConfig['bundleTaskName'] = "createBundle${targetName}JsAndAssets"
        reactConfig['bundleFileResolver'] = getBundleFileResolver([
            "$buildDir/ASSETS/createBundle${targetName}JsAndAssets", // Android Gradle Plugin 7.3
            "$buildDir/generated/assets/createBundle${targetName}JsAndAssets" // Android Gradle Plugin 7.4 and up
        ] as String[], bundleAssetName)
        reactConfig['bundleAssetName'] = bundleAssetName
        reactConfig['root'] = project.extensions.react.root.get() ?: "../../"
    } else if (project.hasProperty("react")) {
        // WARNING: on RN 0.71, `project.react` is an empty map, so this `if` must go after the previous one.
        // Legacy way, before RN 0.71
        def bundleAssetName = project.react.bundleAssetName ?: "index.android.bundle"
        reactConfig['bundleTaskName'] = "bundle${targetName}JsAndAssets"
        reactConfig['bundleFileResolver'] = getBundleFileResolver(["$buildDir/generated/assets/react/${targetPath}"] as String[], bundleAssetName)
        reactConfig['bundleAssetName'] = bundleAssetName
        reactConfig['root'] = project.react.root ?: "../../"
    } else {
        // We assume this cannot happen with RN >= 0.71, so we use the legacy default values
        def bundleAssetName = "index.android.bundle"
        reactConfig['bundleTaskName'] = "bundle${targetName}JsAndAssets"
        reactConfig['bundleFileResolver'] = getBundleFileResolver(["$buildDir/generated/assets/react/${targetPath}"] as String[], bundleAssetName)
        reactConfig['bundleAssetName'] = bundleAssetName
        reactConfig['root'] = "../../"
    }

    return reactConfig
}

private def runShellCommand(
        List<String> command,
        File workingDirectory
) {
    def outputStream = new ByteArrayOutputStream()
    def errorStream = new ByteArrayOutputStream()

    try {
        def result = exec {
            workingDir = workingDirectory
            standardOutput = outputStream
            errorOutput = errorStream
            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                commandLine("cmd", "/c", *command)
            } else {
                commandLine(*command)
            }
        }

        if (result.exitValue != 0) {
            logger.error(errorStream.toString("UTF-8"))
            result.rethrowFailure()
        } else {
            logger.lifecycle(outputStream.toString("UTF-8"))
        }
    } catch (Exception e) {
        def errorStreamContent = errorStream.toString("UTF-8")
        def standardStreamContent = outputStream.toString("UTF-8")
        logger.error("Exception raised during command execution," +
                " stderr=${errorStreamContent}, stdout=${standardStreamContent}")
        throw e
    }
}

private def getServiceName(variant) {
    if (project.hasProperty("datadog")) {
        if (project.datadog.serviceName) {
            return project.datadog.serviceName
        }
    }
    return variant.applicationId
}
jstheoriginal commented 1 year ago

@louiszawadzki here's what you asked for (at least what I could get so far):

  • the changes made by the wizard in the ios directory of your app

here's a walkthrough of what was changed by the wizard. nothing seems off so I'm thinking it must be the datadog-ci tool that is changing something causing the app to crash.

https://www.loom.com/share/0de59a4856974a7888462f1a075d32bb

  • how you initialize the Datadog SDK in your code
import {DdSdkReactNativeConfiguration, SdkVerbosity} from '@datadog/mobile-react-native'
import {useEffect} from 'react'
import Config from 'react-native-config'
import {DatadogCodepush} from '@datadog/mobile-react-native-code-push'
import {getEnv} from '../constants/App'

const config = new DdSdkReactNativeConfiguration(
  Config.DATADOG_CLIENT_TOKEN,
  getEnv(),
  Config.DATADOG_APPLICATION_ID,
  !__DEV__, // track User interactions (e.g.: Tap on buttons)
  !__DEV__, // track XHR Resources
  !__DEV__, // track Errors
)

config.nativeCrashReportEnabled = true
config.trackFrustrations = true
config.serviceName = 'levels_mobile'
config.verbosity = SdkVerbosity.WARN
config.firstPartyHosts = [REDACTED]
// Report JS tasks that take longer than 500ms as long tasks
config.longTaskThresholdMs = 500 // 500ms

const useDataDog = () => {
  useEffect(() => {
    DatadogCodepush.initialize(config)
  }, [])
}

export default useDataDog
  • the stacktrace of the error you are seeing (if it's the native app that crashes, you can find the stacktrace by starting the app from Xcode)

I'm unable to get it to run via Xcode successfully. When it runs the Bundle React Native code and images build phase script, it errors out due to this issue since i use node via nvm only:

Node found at: /Users/justin/.nvm/versions/node/v16.15.0/bin/node
env: node: No such file or directory

(note that it doesn't do this when not using Xcode).

jstheoriginal commented 1 year ago

@louiszawadzki I also see this as a possible issue:

there's this code in the Android script to get the serviceName, but we define the serviceName in js code, so I don't see how it would know that our serviceName is levels_mobile instead of using the application id...do we need to add a project property called datadog with serviceName to our app/build.gradle then to make that work properly for Android? And how would this be done for iOS?

private def getServiceName(variant) {
    if (project.hasProperty("datadog")) {
        if (project.datadog.serviceName) {
            return project.datadog.serviceName
        }
    }
    return variant.applicationId
}

thanks in advance!

louiszawadzki commented 1 year ago

Hi @jstheoriginal, thanks a lot for the very thorough reports! I'll try to answer them all, let me know if I forgot one topic.

Also I have little time this week to investigate, but will definitely be able to look into more details and release fixes next week!

Xcode build issue

When building on Xcode, you get the following error: env: node: No such file or directory. This is a usual issue when React Native is used with nvm. The good news is that with newer versions of RN it can be fixed on your side :)

To do so, you can copy the ios/.xcode.env file into a ios/.xcode.env.local file and there define the path to your local node (it is all explained in ios/.xcode.env).

iOS build issue (from the terminal)

The changes you showed seem alright by me, so having the Xcode build log will definitely help in solving this one. In particular, if you can include the log for the "Bundle React Native code and images" build step that would be great! Hopefully the previous section enables you to build from Xcode, let me know if you have more data on the crash to help us troubleshoot.

I believe this is probably linked to the custom build config as this is not something so common for React Native projects and we don't run automated tests on this yet.

Android sourcemaps issue

It looks like our script doesn't support flavours indeed. I apologize for this, I'm glad you were able to make it work on your end, I will look into it and release a proper fix next week.

Using a custom service name

You can indeed specify a service name, but that requires a little more setup than just using the wizard for now.

We have the documentation for it here. On Android, you can edit your android/app/build.gradle before the apply from: "../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" line to add:

project.ext.datadog = [
    serviceName: "levels_mobile"
]

On iOS, you can edit the datadog-sourcemaps.sh file to add the service name to the command:

DATADOG_XCODE="..node_modules/.bin/datadog-ci react-native xcode --service levels_mobile"

Alternatively, if your service depends on the build config you can set it as a SERVICE_NAME_IOS environment variable.

Again, thanks a lot for your help on this 🙏!

jstheoriginal commented 1 year ago

@louiszawadzki thanks for the help above!

I was able to get the app built via Xcode and running on my phone with sourcemaps showing up in Datadog....

However, it still crashes immediately on launch when building via ci via Firebase App Distribution. It makes no sense to me what might possibly cause this. 🤔

These are the commands CI runs via fastlane.

    prepare(
      type: "adhoc",
      export_method: "ad-hoc",
      configuration: "BetaRelease",
      scheme: 'BetaReleaseLevelsHealth',
      app_identifier: APP_ID_IOS
    )
    firebase_app_distribution(
      app: FIREBASE_APP_IOS,
      groups: "ios-testers",
      release_notes: changelog_from_git_commits(commits_count: 2)
    )

I'm going to do a firebase beta from my machine directly to see if it still happens (to rule out some difference on the ci machine).

jstheoriginal commented 1 year ago

I can't seem to release to firebase beta via my machine. :/

louiszawadzki commented 1 year ago

Hi @jstheoriginal, thanks for getting back to us on this.

I'd like to check out a few hypothesis, could you let us know the following information:

Also feel free to reach out through our support team so we can exchange more information more easily :)

Thanks a lot!

jstheoriginal commented 1 year ago

@louiszawadzki sorry for the delay.

  • version of react-native-code-push you're using

the latest version (8.0.2)

  • does the version of the app you are releasing have a codepush bundle associated with it?

No, it's just a regular adhoc build for firebase beta distribution.

  • a copy or screenshot of your scheme and configuration (mainly how it differs from the Release one)
Screenshot 2023-08-02 at 10 55 08 AM Screenshot 2023-08-02 at 10 57 32 AM

BetaRelease is the one that is crashing on startup.

Feel free to email me directly if you want additional info!

justin at levelshealth dot com

louiszawadzki commented 1 year ago

Hi @jstheoriginal,

Thanks for providing more information! Unfortunately I still can't reproduce the issue on my side.

Could you attach a crash report from the app when it crashes on startup? Seeing what is causing the crash will help us troubleshoot. Here is some documentation on getting the crash logs from a device.

And for additional info and setting up calls, the best be for you to reach out to our support team so they can help with arranging that :) I'll be off for the next few weeks to that will also ensure that the information is properly shared with relevant people. Thanks a lot!