diff --git a/Gemfile b/Gemfile index 995ee1e3ebbb..e64e0667d18c 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'nokogiri' ### Fastlane Plugins +gem 'fastlane-plugin-sentry' gem 'fastlane-plugin-wpmreleasetoolkit', '~> 9.2' # gem 'fastlane-plugin-wpmreleasetoolkit', path: '../../release-toolkit' # gem 'fastlane-plugin-wpmreleasetoolkit', git: 'https://github.com/wordpress-mobile/release-toolkit', branch: '' diff --git a/Gemfile.lock b/Gemfile.lock index 651960877cce..8d2a8b367b52 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -174,6 +174,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-sentry (1.19.0) + os (~> 1.1, >= 1.1.4) fastlane-plugin-wpmreleasetoolkit (9.4.0) activesupport (>= 6.1.7.1) buildkit (~> 1.5) @@ -355,6 +357,7 @@ PLATFORMS DEPENDENCIES danger-dangermattic (~> 1.0) fastlane (~> 2) + fastlane-plugin-sentry fastlane-plugin-wpmreleasetoolkit (~> 9.2) nokogiri rmagick (~> 4.1) diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 810ee938ee9d..019b399b0663 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -713,3 +713,23 @@ if (project.hasProperty("debugStoreFile")) { } } } + +// Copy React Native JavaScript bundle and source map so they can be upload it to the Crash logging +// service during the build process. +android { + applicationVariants.all { variant -> + variant.mergeAssetsProvider.configure { + doLast { + // Copy bundle and source map files + copy { + from(outputDir) + into("${buildDir}/react-native-bundle-source-map") + include("*.bundle", "*.bundle.map") + } + + // Delete source maps + delete(fileTree(dir: outputDir, includes: ['**/*.bundle.map'])) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java index f3802a04d743..a3b23f033a2a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java @@ -45,6 +45,8 @@ import androidx.viewpager.widget.ViewPager; import com.automattic.android.tracks.crashlogging.CrashLogging; +import com.automattic.android.tracks.crashlogging.JsException; +import com.automattic.android.tracks.crashlogging.JsExceptionCallback; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; @@ -3933,4 +3935,8 @@ public LiveData getSavingInProgressDialogVisibility() { @Nullable private SavedInstanceDatabase getDB() { return SavedInstanceDatabase.Companion.getDatabase(WordPress.getContext()); } + + @Override public void onLogJsException(JsException exception, JsExceptionCallback onExceptionSend) { + mCrashLogging.sendJavaScriptReport(exception, onExceptionSend); + } } diff --git a/build.gradle b/build.gradle index 208be6e589f3..9a84775ff357 100644 --- a/build.gradle +++ b/build.gradle @@ -22,8 +22,8 @@ ext { // libs automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' - automatticTracksVersion = '3.4.0' - gutenbergMobileVersion = 'v1.115.0-alpha3' + automatticTracksVersion = '3.5.0' + gutenbergMobileVersion = 'v1.115.0-alpha5' wordPressAztecVersion = 'v2.0' wordPressFluxCVersion = 'trunk-b9ecc708dde74d6cc95aeab42e56fb8067640039' wordPressLoginVersion = '1.14.1' diff --git a/fastlane/lanes/build.rb b/fastlane/lanes/build.rb index 11fb9f4637b2..9cfa6db8f1b0 100644 --- a/fastlane/lanes/build.rb +++ b/fastlane/lanes/build.rb @@ -37,6 +37,7 @@ build_bundle(app: app, version_name: version_name, build_code: current_build_code, flavor: 'Vanilla', buildType: 'Release') upload_build_to_play_store(app: app, version_name: version_name, track: 'production') + upload_gutenberg_sourcemaps(app: app, release_version: version_name) create_gh_release(app: app, version_name: version_name) if options[:create_release] end @@ -105,6 +106,7 @@ build_bundle(app: app, version_name: version_name, build_code: current_build_code, flavor: 'Vanilla', buildType: 'Release') upload_build_to_play_store(app: app, version_name: version_name, track: 'beta') if options[:upload_to_play_store] + upload_gutenberg_sourcemaps(app: app, release_version: version_name) create_gh_release(app: app, version_name: version_name, prerelease: true) if options[:create_release] end @@ -217,6 +219,7 @@ ) upload_prototype_build(product: 'WordPress', version_name: version_name) + upload_gutenberg_sourcemaps(app: 'Wordpress', release_version: version_name) end ##################################################################################### @@ -240,6 +243,7 @@ ) upload_prototype_build(product: 'Jetpack', version_name: version_name) + upload_gutenberg_sourcemaps(app: 'Jetpack', release_version: version_name) end ##################################################################################### @@ -359,4 +363,35 @@ def generate_prototype_build_number "#{branch}-#{commit}" end end + + # Uploads the React Native JavaScript bundle and source map files. + # These files are provided by the Gutenberg Mobile library. + # + # @param [String] app App name, e.g. 'WordPress' or 'Jetpack'. + # @param [String] release_version Release version name to attach the files to in Sentry. + # + def upload_gutenberg_sourcemaps(app:, release_version:) + # Load Sentry properties + sentry_path = File.join(PROJECT_ROOT_FOLDER, 'WordPress', 'src', app.downcase, 'sentry.properties') + sentry_properties = JavaProperties.load(sentry_path) + sentry_token = sentry_properties[:'auth.token'] + project_slug = sentry_properties[:'defaults.project'] + org_slug = sentry_properties[:'defaults.org'] + + # Bundle and source map files are copied to a specific folder as part of the build process. + bundle_source_map_path = File.join(PROJECT_ROOT_FOLDER, 'WordPress', 'build', 'react-native-bundle-source-map') + + sentry_upload_sourcemap( + auth_token: sentry_token, + org_slug: org_slug, + project_slug: project_slug, + version: release_version, + dist: current_build_code, + # When the React native bundle is generated, the source map file references include the local machine path; + # With the `rewrite` and `strip_common_prefix` options, Sentry automatically strips this part. + rewrite: true, + strip_common_prefix: true, + sourcemap: bundle_source_map_path + ) + end end diff --git a/libs/editor/build.gradle b/libs/editor/build.gradle index 1c92342a989d..71cbcb4aaf6e 100644 --- a/libs/editor/build.gradle +++ b/libs/editor/build.gradle @@ -13,6 +13,8 @@ repositories { includeGroup "org.wordpress.aztec" includeGroup "org.wordpress.gutenberg-mobile" includeGroupByRegex "org.wordpress.react-native-libraries.*" + includeGroup "com.automattic" + includeGroup "com.automattic.tracks" } } maven { @@ -104,6 +106,7 @@ dependencies { implementation "com.google.android.material:material:$googleMaterialVersion" implementation "com.android.volley:volley:$androidVolleyVersion" implementation "com.google.code.gson:gson:$googleGsonVersion" + implementation "com.automattic:Automattic-Tracks-Android:$automatticTracksVersion" lintChecks "org.wordpress:lint:$wordPressLintVersion" } diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java b/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java index 3f1f5a568440..1a809ad11c6a 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java @@ -14,6 +14,8 @@ import androidx.lifecycle.LiveData; import com.android.volley.toolbox.ImageLoader; +import com.automattic.android.tracks.crashlogging.JsException; +import com.automattic.android.tracks.crashlogging.JsExceptionCallback; import org.wordpress.android.editor.gutenberg.DialogVisibilityProvider; import org.wordpress.android.util.helpers.MediaFile; @@ -232,6 +234,8 @@ public interface EditorFragmentListener extends DialogVisibilityProvider { void onToggleRedo(boolean isDisabled); void onBackHandlerButton(); + + void onLogJsException(JsException jsException, JsExceptionCallback onSendJsException); } /** diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java index 5d3b930e26e2..b62cf26b94a4 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java @@ -35,6 +35,7 @@ import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidRequestUnsupportedBlockFallbackListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidSendButtonPressedActionListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnImageFullscreenPreviewListener; +import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnLogExceptionListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnReattachMediaUploadQueryListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnFocalPointPickerTooltipShownEventListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnMediaEditorListener; @@ -95,6 +96,7 @@ public void attachToContainer(ViewGroup viewGroup, OnMediaLibraryButtonListener OnToggleRedoButtonListener onToggleRedoButtonListener, OnConnectionStatusEventListener onConnectionStatusEventListener, OnBackHandlerEventListener onBackHandlerEventListener, + OnLogExceptionListener onLogExceptionListener, boolean isDarkMode) { mWPAndroidGlueCode.attachToContainer( viewGroup, @@ -120,6 +122,7 @@ public void attachToContainer(ViewGroup viewGroup, OnMediaLibraryButtonListener onToggleRedoButtonListener, onConnectionStatusEventListener, onBackHandlerEventListener, + onLogExceptionListener, isDarkMode); } diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java index 29199a679fb8..4d2057d509a7 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java @@ -33,6 +33,9 @@ import androidx.lifecycle.LiveData; import com.android.volley.toolbox.ImageLoader; +import com.automattic.android.tracks.crashlogging.JsException; +import com.automattic.android.tracks.crashlogging.JsExceptionCallback; +import com.automattic.android.tracks.crashlogging.JsExceptionStackTraceElement; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableNativeMap; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -47,10 +50,10 @@ import org.wordpress.android.editor.EditorThemeUpdateListener; import org.wordpress.android.editor.LiveTextWatcher; import org.wordpress.android.editor.R; -import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase; import org.wordpress.android.editor.WPGutenbergWebViewActivity; import org.wordpress.android.editor.gutenberg.GutenbergDialogFragment.GutenbergDialogNegativeClickInterface; import org.wordpress.android.editor.gutenberg.GutenbergDialogFragment.GutenbergDialogPositiveClickInterface; +import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.DateTimeUtils; @@ -62,14 +65,16 @@ import org.wordpress.android.util.helpers.MediaFile; import org.wordpress.android.util.helpers.MediaGallery; import org.wordpress.aztec.IHistoryListener; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.LogExceptionCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergEmbedWebViewActivity; +import org.wordpress.mobile.WPAndroidGlue.GutenbergJsException; import org.wordpress.mobile.WPAndroidGlue.Media; import org.wordpress.mobile.WPAndroidGlue.MediaOption; import org.wordpress.mobile.WPAndroidGlue.RequestExecutor; import org.wordpress.mobile.WPAndroidGlue.ShowSuggestionsUtil; import org.wordpress.mobile.WPAndroidGlue.UnsupportedBlock; -import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnBlockTypeImpressionsEventListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnBackHandlerEventListener; +import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnBlockTypeImpressionsEventListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnConnectionStatusEventListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnContentInfoReceivedListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnCustomerSupportOptionsListener; @@ -80,6 +85,7 @@ import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidRequestPreviewListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidRequestUnsupportedBlockFallbackListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidSendButtonPressedActionListener; +import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnLogExceptionListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnMediaLibraryButtonListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnReattachMediaUploadQueryListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnSetFeaturedImageListener; @@ -88,9 +94,11 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import static org.wordpress.mobile.WPAndroidGlue.Media.createRNMediaUsingMimeType; @@ -517,6 +525,40 @@ public void onGotoCustomerSupportOptions() { } }, + new OnLogExceptionListener() { + @Override public void onLogException(GutenbergJsException exception, + LogExceptionCallback logExceptionCallback) { + List stackTraceElements = exception.getStackTrace().stream().map( + stackTrace -> { + return new JsExceptionStackTraceElement( + stackTrace.getFileName(), + stackTrace.getLineNumber(), + stackTrace.getColNumber(), + stackTrace.getFunction() + ); + }).collect(Collectors.toList()); + + JsException jsException = new JsException( + exception.getType(), + exception.getMessage(), + stackTraceElements, + exception.getContext(), + exception.getTags(), + exception.isHandled(), + exception.getHandledBy() + ); + + JsExceptionCallback callback = new JsExceptionCallback() { + @Override + public void onReportSent(boolean success) { + logExceptionCallback.onLogException(success); + } + }; + + mEditorFragmentListener.onLogJsException(jsException, callback); + } + }, + GutenbergUtils.isDarkMode(getActivity())); // request dependency injection. Do this after setting min/max dimensions