Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android error when building with Expo #182

Open
JoshRiser opened this issue Nov 18, 2023 · 14 comments
Open

Android error when building with Expo #182

JoshRiser opened this issue Nov 18, 2023 · 14 comments

Comments

@JoshRiser
Copy link

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
Copy link

jpbast commented Nov 21, 2023

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
Copy link
Contributor

@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:

  • Stick with the 1.0.0 beta. The issues reported here are likely package management related.
  • Whichever environment is used, clear caches. If using eas, try building with --local to diagnose or narrow down the issue.

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
Copy link

vitorfrs-dev commented Dec 12, 2023

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
Copy link
Contributor

@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
Copy link

@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
Copy link

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
Copy link

camsjams commented Aug 9, 2024

@vitorfrs-dev
I'm not seeing this work on [email protected] and [email protected], 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
Copy link

@camsjams, this plugin worked on expo@^49.0.7 and [email protected]. 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 [email protected].


// 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
Copy link

camsjams commented Aug 9, 2024

@vitorfrs-dev amazing!

This last code snippet above does indeed work on [email protected] and [email protected]

Thank You

@danielricecodes
Copy link

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

@camsjams
Copy link

camsjams commented Sep 3, 2024

@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
Copy link

@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
Copy link

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
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants