New Relic の React Native エージェントは、アプリ内で発生した JavaScript エラーや未処理の Promise Rejection を MobileJSError イベントとして記録します。このイベントを用いてエラーの発生状況を分析することができますが、リリースビルドの JavaScript はバンドルされるため、スタックトレースには index.android.bundle:1:406876 のようなバンドル内の位置しか表示されません。そのため、エラーの発生箇所を正確に把握するには、ビルド時に生成されたソースマップをアップロードしてシンボリケートを行う必要があります。

本記事では、Expo を使わない素の React Native プロジェクトをゼロから作成し、iOS と Android それぞれでソースマップの自動アップロードとシンボリケートの動作を確認していきます。
注意点として、ソースマップのアップロードと MobileJSError イベントの利用は、React Native エージェント 1.9.0 以降となります。

検証環境

今回の検証は以下の環境で行いました。

  • React Native 0.87.1(Community CLI で作成、Hermes 有効(デフォルト))
  • newrelic-react-native-agent 1.9.1(iOS エージェント 7.7.7 / Android エージェント 7.8.2)
  • New Relic Android Gradle プラグイン 7.8.2
  • iOS シミュレータ / Android エミュレータ

事前準備

New Relic 側で、以下の 2 つを用意します。どちらも同じアカウントのものを使用してください。

  1. 各 OS 用のアプリケーショントークン
    新規でトークンを発行する場合は、ガイド付きインストールにより、画面の指示に従って進めるとスムーズです。

  2. User API キー(NRAK- から始まるキー)

サンプルアプリの作成

まず、React Native のプロジェクトを作成し、エージェントをインストールします。

npx @react-native-community/cli@latest init NRSourceMapTest --version 0.87.1
cd NRSourceMapTest
npm install newrelic-react-native-agent@1.9.1

エージェントの設定

次に、index.js でエージェントを起動します。New Relic のガイド付きインストールで React Native を選択すると表示されるコードを、テンプレートの index.js に組み込みます。

import NewRelic from 'newrelic-react-native-agent';
import {AppRegistry, Platform} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

let appToken;

if (Platform.OS === 'ios') {
  appToken = '<YOUR_IOS_TOKEN>';
} else {
  appToken = '<YOUR_ANDROID_TOKEN>';
}

const agentConfiguration = {
  // ガイド付きインストールで表示される設定のまま
};

NewRelic.startAgent(appToken, agentConfiguration);
NewRelic.setJSAppVersion('1.0');
AppRegistry.registerComponent(appName, () => App);

エラーを発生させる画面の作成

次に、テンプレートの App.tsx を以下の内容に置き換え、ボタンを押すとエラーを記録する画面を作成します。 シンボリケートが成功していれば、スタックトレースに App.tsx と、エラーを投げた行の行番号が表示されます。

import React from 'react';
import {Button, StyleSheet, View} from 'react-native';
import NewRelic from 'newrelic-react-native-agent';

const onPress = () => {
  try {
    throw new Error('sourcemap check');
  } catch (e) {
    NewRelic.recordError(e as Error);
  }
};

function App(): React.JSX.Element {
  return (
    <View style={styles.container}>
      <Button title="Send JS error" onPress={onPress} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default App;

ソースマップの自動アップロード設定

ソースマップのアップロード方法は、iOS と Android で異なります。

iOS

iOS では、エージェントに同梱されている upload-react-native-sourcemap スクリプトを Xcode のビルドフェーズから実行し、ソースマップをアップロードします。

まず、エージェントの iOS 向けネイティブモジュールをプロジェクトに組み込むため、pod install を実行します。

cd ios && pod install && cd ..

次に、スクリプト一式を ios フォルダにコピーします。

cp -R node_modules/newrelic-react-native-agent/plugin/dsym-upload-tools ios/

続いて、Xcode で ios/NRSourceMapTest.xcworkspace を開き、アプリのターゲットの Build Phases を表示します。 既存の Bundle React Native code and images フェーズのスクリプト先頭に、以下の 1 行を追加します。 iOS ではデフォルトでソースマップが出力されないため、SOURCEMAP_FILE を明示的に設定する必要があります。

export SOURCEMAP_FILE="$DERIVED_FILE_DIR/main.jsbundle.map"

さらに、New Run Script Phase を追加し、Bundle React Native code and images の後ろに配置します。 1 行目の NEWRELIC_SOURCEMAP_ALLOW_SIMULATOR=true は、シミュレータ向けのビルドでもアップロードを行うための設定です。実機でのみ確認する場合は不要です。

export NEWRELIC_SOURCEMAP_ALLOW_SIMULATOR=true
ARTIFACT_DIR="${BUILD_DIR%Build/*}"
SCRIPT=`/usr/bin/find "${SRCROOT}" "${ARTIFACT_DIR}" -type f -name upload-react-native-sourcemap | head -n 1`
/bin/sh "${SCRIPT}" "<USER_API_KEY>" "<IOS_APP_TOKEN>"

Android

Android では、New Relic Android エージェントの Gradle プラグインが、ビルド時にソースマップをアップロードします。

まず、android/build.gradle の buildscript の dependencies に、Gradle プラグインを追加します。バージョンは、React Native エージェントに含まれる Android エージェントのバージョン(今回は 7.8.2)に合わせます。

buildscript {
    dependencies {
        // 既存の classpath はそのまま
        classpath("com.newrelic.agent.android:agent-gradle-plugin:7.8.2")
    }
}

次に、android/app/build.gradle でプラグインを適用します。既存の apply plugin の行の後に追加します。

apply plugin: "newrelic"

最後に、android/app/newrelic.properties を作成し、User API キーと Android 用のアプリケーショントークンを設定します。

com.newrelic.api_key=<USER_API_KEY>
com.newrelic.application_token=<ANDROID_APP_TOKEN>

なお、本手順は検証のため User API キーをプロジェクトファイルに直接記述していますが、実際に運用される際はそのまま Git リポジトリにコミットしないよう注意してください。

ビルド

それでは、ビルドしてみましょう。iOS、Android ともにソースマップがアップロードされるのはリリースビルドのみのため、--mode Release を指定して実行します。

npx react-native run-ios --mode Release
npx react-native run-android --mode Release

New Relic での確認

アプリを起動してボタンを押し、以下の NRQL でイベントが届いているかを確認します。イベントは一定間隔でまとめて送信されるため、反映まで数分かかることがあります。

SELECT * FROM MobileJSError SINCE 30 minutes ago

JavaScript errorsの詳細画面から、スタックトレースがシンボリケートされていることも確認できます。

アップロード済みのソースマップは、API で一覧表示・削除できます。詳しくは「List and delete React Native source maps」のドキュメントを参照してください。

まとめ

本記事では、素の React Native プロジェクトを使って、iOS と Android で JavaScript のソースマップを自動アップロードし、MobileJSError のスタックトレースをシンボリケートする流れを紹介いたしました。 CodePush などの OTA で JavaScript だけを更新する場合は、cURL やスクリプトで手動アップロードする方法も用意されています。詳しくは以下のドキュメントを参照のうえ、ぜひ試してみてください。