rollbar / rollbar-react-native

Crash and error reporting for React Native using Rollbar
https://docs.rollbar.com/docs/react-native
62 stars 39 forks source link

Android error when building with Expo #182

Open JoshRiser opened 11 months ago

JoshRiser commented 11 months ago

When trying to build my Expo managed app on Android, I run into the following error:

Attempt to invoke virtual method 'void com.rollbar.android.Rollbar.configure(com.rollbar.notifier.config.ConfigProvider)' on a null object reference'

(same as a closed issue here)

I also found this closed issue, where waltjones gave a solution, which I tried on version 1.0.0-beta.1, 0.9.0 (where there was a PR related to walt's solution,) and 0.9.1 (the version at the time of walt's solution.)

I tried these two configuration options:

const rollbarNative = new Client({
  accessToken: constants.ROLLBAR_KEY,
  environment: 'staging',
  captureUncaught: true,
  captureUnhandledRejections: true,
  captureDeviceInfo: true
});
const rollbar = rollbarNative.rollbar;

and

const config = new Configuration(constants.ROLLBAR_KEY, {
  environment: 'staging',
  captureUncaught: true,
  captureUnhandledRejections: true,
  captureDeviceInfo: true
});
const rollbar = new Client(config);

And neither of them work.

I am also trying to use this configuration with the Provider and ErrorBoundary if that makes a difference.

It all works fine on iOS but Android doesn't want to work.

jpbast commented 11 months ago

I'm having the same issue here. One thing that I tried to do was running prebuild and then add this at MainApplication.java. But now I'm having the following error message: C:\${PATH_TO_PROJECT}\android\gradlew.bat exited with non-zero code: 1

waltjones commented 10 months ago

@JoshRiser The missing reference should be present. Your error might be related to mismatched cached packages either on a legacy expo build, or a cloud eas build. (As far as I can tell, eas local builds don't cache anything.)

@jpbast If you can provide the relevant logging (either local or cloud), that will help to diagnose.

In both cases, since the eas build process is so different from the legacy expo build, and the local and cloud eas builds work differently, it's helpful to specify which type of build. It may also be relevant if the app was converted from expo to eas, and what versions of expo and eas-cli are used.

Some general recommendations:

I have tested this first using the defaults created by create-expo-app, this creates legacy non-eas expo app, and followed the doc for the 1.0.0 beta. Build and run using npx expo run:android.

package.json dependencies

  "dependencies": {
    "expo": "~49.0.15",
    "expo-splash-screen": "~0.20.5",
    "expo-status-bar": "~1.6.0",
    "react": "18.2.0",
    "react-native": "0.72.6",
    "rollbar": "^2.26.2",
    "rollbar-react-native": "^1.0.0-beta.1"
  },

This works for me, with no build or runtime errors, and I can send Rollbar messages from the Java app.

Then I converted to an eas app eas build:configure, and built locally eas build -p android --local. Uninstall the previous build before running the eas app. adb shell pm uninstall com.waltjones.expoeas Run the eas app. eas build:run -p android

This also worked with no issues. And lastly I did this using the cloud build with no errors.

vitorfrs-dev commented 10 months ago

I was having a similar error. Without changing any native files, I could build the app on EAS, but the app was crashing every time I opened it.

Then I built a development build, and the app was still crashing, but now I could see the error message.

Captura de Tela 2023-12-12 às 10 41 59


I managed to make it work by creating a config plugin

// plugins/rollbar-config-plugin.js

const { withMainApplication, AndroidConfig } = require('@expo/config-plugins');

const withRollbarAndroid = (config, { rollbarPostToken, environment }) => {
  return withRollbarMainApplication(config, { rollbarPostToken, environment });
};

const withRollbarMainApplication = (config, { rollbarPostToken, environment }) => {
  return withMainApplication(config, async (config) => {
    config.modResults.contents = modifyMainApplication({
      contents: config.modResults.contents,
      rollbarPostToken,
      environment,
      packageName: AndroidConfig.Package.getPackage(config),
    });

    return config;
  });
};

const modifyMainApplication = ({ contents, rollbarPostToken, packageName, environment }) => {
  if (!packageName) {
    throw new Error('Android package not found');
  }

  const importLine = `import com.rollbar.RollbarReactNative;`;
  if (!contents.includes(importLine)) {
    const packageImport = `package ${packageName};`;
    // Add the import line to the top of the file
    // Replace the first line with the rollbar import
    contents = contents.replace(`${packageImport}`, `${packageImport}\n${importLine}`);
  }

  const initLine = `RollbarReactNative.init(this, "${rollbarPostToken}", "${environment}");`;

  if (!contents.includes(initLine)) {
    const soLoaderLine = `SoLoader.init(this, /* native exopackage */ false);`;
    // Replace the line SoLoader.init(this, /* native exopackage */ false); with regex
    contents = contents.replace(`${soLoaderLine}`, `${soLoaderLine}\n\t\t${initLine}\n`);
  }

  return contents;
};

const withRollbar = (config, options) => {
  config = withRollbarAndroid(config, options);

  return config;
};

module.exports = withRollbar;

Then on your app.config.ts or app.json you can use the plugin:

{
   plugins: {
     ['./plugins/rollbar-config.plugin', {
       environment: "production",
       rollbarPostToken: "POST_CLIENT_ITEM_ACCESS_TOKEN"
     }]
   }
}

This also enables Rollbar to log crashes in the native code (Android)

waltjones commented 10 months ago

@vitorfrs-dev It's good that this works for you. The doc suggests adding import com.rollbar.RollbarReactNative; to MainApplication.java, which will be simpler for most people.

Crane101 commented 9 months ago

@waltjones if we're using expo we do not have direct access to MainApplication.java when building for release - without ejecting we can only get fingers in there via a plugin as far as I know.

Did get it working using your plugin though @vitorfrs-dev, kudos 🙌

Maybe worth considering adding some version of this to the repo to support expo users?

guilherme-vp commented 6 months ago

I'd also love to see an official Expo plugin for Rollbar. In our app, it's the only library we still have to support native configuration manually. Having it configured with Expo would be amazing!

camsjams commented 2 months ago

@vitorfrs-dev I'm not seeing this work on expo@51.0.26 and rollbar-react-native@1.0.0-beta.3, care to share version numbers? And was this done via eas cloud build?

@waltjones From what I understand, aside from the plugin option mentioned above, there is no alternative for getting rollbar-react-native to work with the Provider/ error boundary on a vanilla Expo-based project?

vitorfrs-dev commented 2 months ago

@camsjams, this plugin worked on expo@^49.0.7 and rollbar-react-native@1.0.0-beta.1. Starting on Expo SDK 50, react native changed from using MainApplication.java to MainApplication.kt. You might be experiencing issues because the plugin utilizes Java syntax instead of Kotlin.

See the Expo 50 breaking changes

React Native 0.73 changed from Java to Kotlin for Android Main* classes: MainApplication.java/MainActivity.java are now MainApplication.kt/MainActivity.kt. If you depend on any config plugins that use dangerous modifications to change these files, they may need to be updated for SDK 50 support.

You can try using the plugin with the Kolin syntax, but I only tested it with expo@50 and rollbar-react-native@1.0.0-beta.2.


// plugins/rollbar-config-plugin/withRollbarAndroid.js
const { withMainApplication, AndroidConfig } = require('@expo/config-plugins');

const withRollbarAndroid = (config, { rollbarPostToken, environment }) => {
  return withRollbarMainApplication(config, { rollbarPostToken, environment });
};

const withRollbarMainApplication = (_config, { rollbarPostToken, environment }) => {
  return withMainApplication(_config, async (config) => {
    config.modResults.contents = modifyMainApplication({
      contents: config.modResults.contents,
      rollbarPostToken,
      environment,
      packageName: AndroidConfig.Package.getPackage(config),
    });

    return config;
  });
};

const modifyMainApplication = ({ contents, rollbarPostToken, packageName, environment }) => {
  if (!packageName) {
    throw new Error('Android package not found');
  }

  const importLine = `import com.rollbar.RollbarReactNative`;
  if (!contents.includes(importLine)) {
    const packageImport = `package ${packageName}`;
    // Add the import line to the top of the file
    // Replace the first line with the rollbar import
    contents = contents.replace(`${packageImport}`, `${packageImport}\n${importLine}`);
  }

  const initLine = `RollbarReactNative.init(this, "${rollbarPostToken}", "${environment}")`;

  if (!contents.includes(initLine)) {
    const soLoaderLine = 'SoLoader.init(this, false)';
    // Replace the line SoLoader.init(this, /* native exopackage */ false); with regex
    contents = contents.replace(`${soLoaderLine}`, `${soLoaderLine}\n\t\t${initLine}\n`);
  }

  return contents;
};

module.exports = withRollbarAndroid;
// plugins/rollbar-config-plugin/index.js
const withRollbarAndroid = require('./withRollbarAndroid');

const withRollbar = (config, options) => {
  config = withRollbarAndroid(config, options);

  return config;
};

module.exports = withRollbar;

// app.config.ts
{
   plugins: {
     ['./plugins/rollbar-config-plugin', {
       environment: "production",
       rollbarPostToken: process.env.POST_CLIENT_ITEM_ACCESS_TOKEN
     }]
   }
}
camsjams commented 2 months ago

@vitorfrs-dev amazing!

This last code snippet above does indeed work on expo@51.0.26 and rollbar-react-native@1.0.0-beta.3

Thank You

danielricecodes commented 1 month ago

The plugin above did not work with Expo 51. Going back to Bugsnag. Sorry.

camsjams commented 1 month ago

@danielricecodes

The plugin above did not work with Expo 51. Going back to Bugsnag. Sorry.

Not sure if you saw above, but it does work if you use the plugin route on Expo 51. It would be nice to have added into main package, I agree.

But I'd much rather use Rollbar over Sentry or Bugsnag. Good luck!

danielricecodes commented 1 month ago

@danielricecodes

The plugin above did not work with Expo 51. Going back to Bugsnag. Sorry.

Not sure if you saw above, but it does work if you use the plugin route on Expo 51. It would be nice to have added into main package, I agree.

But I'd much rather use Rollbar over Sentry or Bugsnag. Good luck!

I ran out of time to debug it but the code didn't work and my compiled app in the internal test track still crashed upon start. I don't know enough about all the intricacies of how Android apps are built to resolve it, so I just posted as a warning to others. The code above was not the saving grace I thought it would be. I don't know why. If someone can post steps to perhaps help me debug and resolve, I'm keen to try again.

guilherme-vp commented 1 month ago

After a few months, since I last checked this thread, it seems like the team has been away for the future of this SDK 🫠 Here is a complete version of @vitorfrs-dev plugin considering iOS. Note that I used a similar approach for Android Java, this should not work with Kotlin, but the implementation for that should not be too hard considering it's only a "find and replace" implementation.

const {
  withMainApplication,
  withAppDelegate,
  AndroidConfig,
  IOSConfig,
} = require('@expo/config-plugins');

/**
 * Options for the Rollbar plugin.
 * @typedef {Object} PluginOptions
 * @property {string} rollbarPostToken - The Rollbar post token for API access.
 * @property {string} [environment] - The environment in which the app is running.
 */

const DEFAULT_APP_NAME = '<your-app-name>';

/**
 * Configures Rollbar for Android by modifying the MainApplication.java file.
 *
 * @param {Object} config - The Expo configuration object.
 * @param {PluginOptions} options - The options for the Rollbar plugin.
 * @returns {Object} The modified Expo configuration object.
 */
const withRollbarAndroid = (config, {rollbarPostToken, environment}) => {
  return withMainApplication(config, (props) => {
    props.modResults.contents = modifyMainApplication({
      contents: props.modResults.contents,
      rollbarPostToken,
      environment,
      packageName: AndroidConfig.Package.getPackage(config) ?? DEFAULT_APP_NAME,
    });
    return props;
  });
};

/**
 * Modifies the MainApplication.java file to include Rollbar initialization for Android.
 *
 * @param {Object} options
 * @param {string} options.contents - The contents of the MainApplication.java file.
 * @param {string} options.rollbarPostToken - The Rollbar post access token.
 * @param {string} options.packageName - The Android package name. Defaults to "<your-app-name>".
 * @param {string} [options.environment] - The environment in which the app is running.
 * @returns {string} The modified contents of the MainApplication.java file.
 */
const modifyMainApplication = ({contents, rollbarPostToken, packageName, environment}) => {
  const importLine = `import com.rollbar.RollbarReactNative;`;
  if (!contents.includes(importLine)) {
    const packageImport = `package ${packageName};`;
    contents = contents.replace(`${packageImport}`, `${packageImport}\n${importLine}`);
  }

  const initLine = `RollbarReactNative.init(this, "${rollbarPostToken}", "${environment}");`;

  if (!contents.includes(initLine)) {
    const soLoaderLine = `SoLoader.init(this, /* native exopackage */ false);`;
    contents = contents.replace(`${soLoaderLine}`, `${soLoaderLine}\n\t\t${initLine}\n`);
  }

  return contents;
};

/**
 * Configures Rollbar for iOS by modifying the AppDelegate.m file.
 *
 * @param {Object} config - The Expo configuration object.
 * @param {PluginOptions} options - The options for the Rollbar plugin.
 * @returns {Object} The modified Expo configuration object.
 */
const withRollbariOS = (config, {rollbarPostToken}) => {
  return withAppDelegate(config, (props) => {
    props.modResults.contents = modifyAppDelegate({
      contents: props.modResults.contents,
      rollbarPostToken,
      bundleIdentifier: IOSConfig.BundleIdentifier.getBundleIdentifier(config) ?? DEFAULT_APP_NAME,
    });
    return props;
  });
};

/**
 * Modifies the AppDelegate.m file to include Rollbar initialization for iOS.
 *
 * @param {Object} options
 * @param {string} options.contents - The contents of the MainApplication.java file.
 * @param {string} options.rollbarPostToken - The Rollbar post access token.
 * @param {string} options.bundleIdentifier - The iOS bundle identifier. Defaults to "<your-app-name>"..
 * @returns {string} The modified contents of the AppDelegate.m file.
 */
const modifyAppDelegate = ({contents, rollbarPostToken, bundleIdentifier}) => {
  const importLine = `#import <RollbarReactNative/RollbarReactNative.h>`;
  if (!contents.includes(importLine)) {
    const appDelegateImport = `#import "AppDelegate.h"`;
    contents = contents.replace(`${appDelegateImport}`, `${appDelegateImport}\n${importLine}`);
  }

  const initLine = `NSDictionary *options = @{
    @"accessToken": @"${rollbarPostToken}"
  };
  [RollbarReactNative initWithConfiguration:options];`;

  if (!contents.includes(initLine)) {
    const selfModuleLine = `self.moduleName = @"main";`;
    contents = contents.replace(`${selfModuleLine}`, `${selfModuleLine}\n\t\t${initLine}\n`);
  }

  return contents;
};

/**
 * Adds Rollbar support for both Android and iOS platforms via Expo plugin.
 *
 * @param {Object} config - The Expo configuration object from ../app.config.ts
 * @param {PluginOptions} options - The options for the Rollbar plugin.
 * @returns {Object} The modified Expo configuration object with Rollbar integrated.
 */
const withRollbar = (config, options) => {
  if (!rollbarPostToken) {
    console.warn('No Rollbar Post token given, skipping...');
    return config;
  }

  config = withRollbarAndroid(config, options);
  config = withRollbariOS(config, options);

  return config;
};

module.exports = withRollbar;
guilherme-vp commented 1 month ago

After a few months, since I last checked this thread, it seems like the team has been away for the future of this SDK 🫠 Here is a complete version of @vitorfrs-dev plugin considering iOS. Note that I used a similar approach for Android Java, this should not work with Kotlin, but the implementation for that should not be too hard considering it's only a "find and replace" implementation.

const {
  withMainApplication,
  withAppDelegate,
  AndroidConfig,
  IOSConfig,
} = require('@expo/config-plugins');

/**
 * Options for the Rollbar plugin.
 * @typedef {Object} PluginOptions
 * @property {string} rollbarPostToken - The Rollbar post token for API access.
 * @property {string} [environment] - The environment in which the app is running.
 */

const DEFAULT_APP_NAME = '<your-app-name>';

/**
 * Configures Rollbar for Android by modifying the MainApplication.java file.
 *
 * @param {Object} config - The Expo configuration object.
 * @param {PluginOptions} options - The options for the Rollbar plugin.
 * @returns {Object} The modified Expo configuration object.
 */
const withRollbarAndroid = (config, {rollbarPostToken, environment}) => {
  return withMainApplication(config, (props) => {
    props.modResults.contents = modifyMainApplication({
      contents: props.modResults.contents,
      rollbarPostToken,
      environment,
      packageName: AndroidConfig.Package.getPackage(config) ?? DEFAULT_APP_NAME,
    });
    return props;
  });
};

/**
 * Modifies the MainApplication.java file to include Rollbar initialization for Android.
 *
 * @param {Object} options
 * @param {string} options.contents - The contents of the MainApplication.java file.
 * @param {string} options.rollbarPostToken - The Rollbar post access token.
 * @param {string} options.packageName - The Android package name. Defaults to "<your-app-name>".
 * @param {string} [options.environment] - The environment in which the app is running.
 * @returns {string} The modified contents of the MainApplication.java file.
 */
const modifyMainApplication = ({contents, rollbarPostToken, packageName, environment}) => {
  const importLine = `import com.rollbar.RollbarReactNative;`;
  if (!contents.includes(importLine)) {
    const packageImport = `package ${packageName};`;
    contents = contents.replace(`${packageImport}`, `${packageImport}\n${importLine}`);
  }

  const initLine = `RollbarReactNative.init(this, "${rollbarPostToken}", "${environment}");`;

  if (!contents.includes(initLine)) {
    const soLoaderLine = `SoLoader.init(this, /* native exopackage */ false);`;
    contents = contents.replace(`${soLoaderLine}`, `${soLoaderLine}\n\t\t${initLine}\n`);
  }

  return contents;
};

/**
 * Configures Rollbar for iOS by modifying the AppDelegate.m file.
 *
 * @param {Object} config - The Expo configuration object.
 * @param {PluginOptions} options - The options for the Rollbar plugin.
 * @returns {Object} The modified Expo configuration object.
 */
const withRollbariOS = (config, {rollbarPostToken}) => {
  return withAppDelegate(config, (props) => {
    props.modResults.contents = modifyAppDelegate({
      contents: props.modResults.contents,
      rollbarPostToken,
      bundleIdentifier: IOSConfig.BundleIdentifier.getBundleIdentifier(config) ?? DEFAULT_APP_NAME,
    });
    return props;
  });
};

/**
 * Modifies the AppDelegate.m file to include Rollbar initialization for iOS.
 *
 * @param {Object} options
 * @param {string} options.contents - The contents of the MainApplication.java file.
 * @param {string} options.rollbarPostToken - The Rollbar post access token.
 * @param {string} options.bundleIdentifier - The iOS bundle identifier. Defaults to "<your-app-name>"..
 * @returns {string} The modified contents of the AppDelegate.m file.
 */
const modifyAppDelegate = ({contents, rollbarPostToken, bundleIdentifier}) => {
  const importLine = `#import <RollbarReactNative/RollbarReactNative.h>`;
  if (!contents.includes(importLine)) {
    const appDelegateImport = `#import "AppDelegate.h"`;
    contents = contents.replace(`${appDelegateImport}`, `${appDelegateImport}\n${importLine}`);
  }

  const initLine = `NSDictionary *options = @{
    @"accessToken": @"${rollbarPostToken}"
  };
  [RollbarReactNative initWithConfiguration:options];`;

  if (!contents.includes(initLine)) {
    const selfModuleLine = `self.moduleName = @"main";`;
    contents = contents.replace(`${selfModuleLine}`, `${selfModuleLine}\n\t\t${initLine}\n`);
  }

  return contents;
};

/**
 * Adds Rollbar support for both Android and iOS platforms via Expo plugin.
 *
 * @param {Object} config - The Expo configuration object from ../app.config.ts
 * @param {PluginOptions} options - The options for the Rollbar plugin.
 * @returns {Object} The modified Expo configuration object with Rollbar integrated.
 */
const withRollbar = (config, options) => {
  if (!rollbarPostToken) {
    console.warn('No Rollbar Post token given, skipping...');
    return config;
  }

  config = withRollbarAndroid(config, options);
  config = withRollbariOS(config, options);

  return config;
};

module.exports = withRollbar;

I couldn't expand this plugin considering Kotlin and Swift because I don't have enough expertise for that, it'd be one of the last parts before contributing with an official plugin for the project.