diff --git a/.gitignore b/.gitignore index dd531e21b1..2ec5b98278 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,15 @@ Carthage/Checkouts **/Pods # Fastlane -fastlane/test_output/ -report.xml -.env +appium/fastlane/test_output/ +appium/fastlane/report.xml +appium/.env +appium/fastlane/README.md + +# Appium + +aws +test_bundle.zip +__pycache__ +.cache +wheelhouse diff --git a/.gitmodules b/.gitmodules index f3471b3d00..7da0204e1a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "ios/KSCrash"] path = ios/KSCrash url=https://github.com/kstenerud/KSCrash +[submodule "examples"] + path = examples + url = https://github.com/getsentry/examples diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..fe6165d732 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,47 @@ +matrix: + include: + - language: android + os: linux + jdk: oraclejdk8 + before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -rf $HOME/.gradle/caches/*/plugin-resolution/ + cache: + directories: + - $HOME/.yarn-cache + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + sudo: required + before_install: + - nvm install 7 + - node --version + - travis_retry curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - + - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + - travis_retry sudo apt-get update -qq + - travis_retry sudo apt-get install -y -qq yarn + android: + components: + - build-tools-23.0.1 + - android-23 + - extra-android-m2repository + - extra-google-google_play_services + - extra-google-m2repository + - addon-google_apis-google-16 + script: + - .travis/run.sh + - language: objective-c + os: osx + osx_image: xcode8.3 + cache: + - bundler + - pip + env: + - LANE='ios' + before_install: + - brew update + - brew install yarn + - brew outdated yarn || brew upgrade yarn + script: + - .travis/run.sh +notifications: + email: false diff --git a/.travis/run.sh b/.travis/run.sh new file mode 100755 index 0000000000..76fa2f5a11 --- /dev/null +++ b/.travis/run.sh @@ -0,0 +1,11 @@ +#!/bin/sh +cd appium +bundle install +pip wheel --wheel-dir wheelhouse -r requirements.txt + +if [ "$LANE" = "ios" ]; +then + make test +else + make test-android +fi diff --git a/.vscode/.ropeproject/config.py b/.vscode/.ropeproject/config.py new file mode 100644 index 0000000000..d1e8f3df19 --- /dev/null +++ b/.vscode/.ropeproject/config.py @@ -0,0 +1,100 @@ +# The default ``config.py`` +# flake8: noqa + + +def set_prefs(prefs): + """This function is called before opening the project""" + + # Specify which files and folders to ignore in the project. + # Changes to ignored resources are not added to the history and + # VCSs. Also they are not returned in `Project.get_files()`. + # Note that ``?`` and ``*`` match all characters but slashes. + # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' + # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' + # '.svn': matches 'pkg/.svn' and all of its children + # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' + # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' + prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', + '.hg', '.svn', '_svn', '.git', '.tox'] + + # Specifies which files should be considered python files. It is + # useful when you have scripts inside your project. Only files + # ending with ``.py`` are considered to be python files by + # default. + #prefs['python_files'] = ['*.py'] + + # Custom source folders: By default rope searches the project + # for finding source folders (folders that should be searched + # for finding modules). You can add paths to that list. Note + # that rope guesses project source folders correctly most of the + # time; use this if you have any problems. + # The folders should be relative to project root and use '/' for + # separating folders regardless of the platform rope is running on. + # 'src/my_source_folder' for instance. + #prefs.add('source_folders', 'src') + + # You can extend python path for looking up modules + #prefs.add('python_path', '~/python/') + + # Should rope save object information or not. + prefs['save_objectdb'] = True + prefs['compress_objectdb'] = False + + # If `True`, rope analyzes each module when it is being saved. + prefs['automatic_soa'] = True + # The depth of calls to follow in static object analysis + prefs['soa_followed_calls'] = 0 + + # If `False` when running modules or unit tests "dynamic object + # analysis" is turned off. This makes them much faster. + prefs['perform_doa'] = True + + # Rope can check the validity of its object DB when running. + prefs['validate_objectdb'] = True + + # How many undos to hold? + prefs['max_history_items'] = 32 + + # Shows whether to save history across sessions. + prefs['save_history'] = True + prefs['compress_history'] = False + + # Set the number spaces used for indenting. According to + # :PEP:`8`, it is best to use 4 spaces. Since most of rope's + # unit-tests use 4 spaces it is more reliable, too. + prefs['indent_size'] = 4 + + # Builtin and c-extension modules that are allowed to be imported + # and inspected by rope. + prefs['extension_modules'] = [] + + # Add all standard c-extensions to extension_modules list. + prefs['import_dynload_stdmods'] = True + + # If `True` modules with syntax errors are considered to be empty. + # The default value is `False`; When `False` syntax errors raise + # `rope.base.exceptions.ModuleSyntaxError` exception. + prefs['ignore_syntax_errors'] = False + + # If `True`, rope ignores unresolvable imports. Otherwise, they + # appear in the importing namespace. + prefs['ignore_bad_imports'] = False + + # If `True`, rope will insert new module imports as + # `from import ` by default. + prefs['prefer_module_from_imports'] = False + + # If `True`, rope will transform a comma list of imports into + # multiple separate import statements when organizing + # imports. + prefs['split_imports'] = False + + # If `True`, rope will sort imports alphabetically by module name + # instead of alphabetically by import statement, with from imports + # after normal imports. + prefs['sort_imports_alphabetically'] = False + + +def project_opened(project): + """This function is called after opening the project""" + # Do whatever you like here! diff --git a/.vscode/settings.json b/.vscode/settings.json index 59b0caf005..faf327b08b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,8 @@ "files.trimTrailingWhitespace": true, "files.ensureSingleFinalNewline": true, + "python.linting.pylintEnabled": false, + "prettier.bracketSpacing": false, "prettier.singleQuote": true, "prettier.printWidth": 90, diff --git a/README.md b/README.md index 0dc6bffea5..f34d727726 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@
-

react-native-sentry

+

Sentry SDK for React Native

+[![Travis](https://img.shields.io/travis/getsentry/react-native-sentry.svg?maxAge=2592000)](https://travis-ci.org/getsentry/react-native-sentry) [![npm version](https://img.shields.io/npm/v/react-native-sentry.svg)](https://www.npmjs.com/package/react-native-sentry) [![npm dm](https://img.shields.io/npm/dm/react-native-sentry.svg)](https://www.npmjs.com/package/react-native-sentry) [![npm dt](https://img.shields.io/npm/dt/react-native-sentry.svg)](https://www.npmjs.com/package/react-native-sentry) diff --git a/SentryReactNative.podspec b/SentryReactNative.podspec index 40e2e79b4f..4210ebe14b 100644 --- a/SentryReactNative.podspec +++ b/SentryReactNative.podspec @@ -18,8 +18,8 @@ Pod::Spec.new do |s| s.preserve_paths = '*.js' s.dependency 'React' - s.dependency 'Sentry', '~> 3.1.3' - s.dependency 'Sentry/KSCrash', '~> 3.1.3' + s.dependency 'Sentry', '~> 3.3.3' + s.dependency 'Sentry/KSCrash', '~> 3.3.3' s.source_files = 'ios/RNSentry*.{h,m}' s.public_header_files = 'ios/RNSentry.h' diff --git a/android/src/main/java/io/sentry/RNSentryEventEmitter.java b/android/src/main/java/io/sentry/RNSentryEventEmitter.java index 7604dd8fc0..d03c324409 100644 --- a/android/src/main/java/io/sentry/RNSentryEventEmitter.java +++ b/android/src/main/java/io/sentry/RNSentryEventEmitter.java @@ -11,6 +11,7 @@ public class RNSentryEventEmitter extends ReactContextBaseJavaModule { public static final String SENTRY_EVENT_SENT_SUCCESSFULLY = "Sentry/eventSentSuccessfully"; + public static final String SENTRY_EVENT_STORED = "Sentry/eventStored"; public RNSentryEventEmitter(ReactApplicationContext reactContext) { super(reactContext); @@ -25,6 +26,7 @@ public String getName() { public Map getConstants() { final Map constants = new HashMap<>(); constants.put("EVENT_SENT_SUCCESSFULLY", SENTRY_EVENT_SENT_SUCCESSFULLY); + constants.put("EVENT_STORED", SENTRY_EVENT_STORED); return constants; } diff --git a/android/src/main/java/io/sentry/RNSentryModule.java b/android/src/main/java/io/sentry/RNSentryModule.java index cac91226bd..c4d6000cf4 100644 --- a/android/src/main/java/io/sentry/RNSentryModule.java +++ b/android/src/main/java/io/sentry/RNSentryModule.java @@ -231,6 +231,7 @@ public void captureEvent(ReadableMap event) { } Sentry.capture(buildEvent(eventBuilder)); + RNSentryEventEmitter.sendEvent(reactContext, RNSentryEventEmitter.SENTRY_EVENT_STORED, new WritableNativeMap()); } @ReactMethod diff --git a/appium/Gemfile b/appium/Gemfile new file mode 100644 index 0000000000..b734015f82 --- /dev/null +++ b/appium/Gemfile @@ -0,0 +1,10 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +source "https://rubygems.org" + +gem 'fastlane' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/appium/Gemfile.lock b/appium/Gemfile.lock new file mode 100644 index 0000000000..0dc351d578 --- /dev/null +++ b/appium/Gemfile.lock @@ -0,0 +1,150 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (2.3.5) + addressable (2.5.1) + public_suffix (~> 2.0, >= 2.0.2) + aws-sdk (2.10.17) + aws-sdk-resources (= 2.10.17) + aws-sdk-core (2.10.17) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-resources (2.10.17) + aws-sdk-core (= 2.10.17) + aws-sigv4 (1.0.1) + babosa (1.0.2) + claide (1.0.2) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.5) + highline (~> 1.7.2) + declarative (0.0.9) + declarative-option (0.1.0) + domain_name (0.5.20170404) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.2.1) + excon (0.57.1) + faraday (0.12.2) + multipart-post (>= 1.2, < 3) + faraday-cookie_jar (0.0.6) + faraday (>= 0.7.4) + http-cookie (~> 1.0.0) + faraday_middleware (0.11.0.1) + faraday (>= 0.7.4, < 1.0) + fastimage (2.1.0) + fastlane (2.49.0) + CFPropertyList (>= 2.3, < 3.0.0) + addressable (>= 2.3, < 3.0.0) + babosa (>= 1.0.2, < 2.0.0) + bundler (>= 1.12.0, < 2.0.0) + colored + commander-fastlane (>= 4.4.5, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + excon (>= 0.45.0, < 1.0.0) + faraday (~> 0.9) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 0.9) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.0.1, < 2.0.0) + google-api-client (>= 0.12.0, < 0.13.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + mini_magick (~> 4.5.1) + multi_json + multi_xml (~> 0.5) + multipart-post (~> 2.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 1.1.0, < 2.0.0) + security (= 0.1.3) + slack-notifier (>= 1.3, < 2.0.0) + terminal-notifier (>= 1.6.2, < 2.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (~> 0.5.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.4.4, < 2.0.0) + xcpretty (>= 0.2.4, < 1.0.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-aws_device_farm (0.3.0) + aws-sdk + gh_inspector (1.0.3) + google-api-client (0.12.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.5) + httpclient (>= 2.8.1, < 3.0) + mime-types (~> 3.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + googleauth (0.5.3) + faraday (~> 0.12) + jwt (~> 1.4) + logging (~> 2.0) + memoist (~> 0.12) + multi_json (~> 1.11) + os (~> 0.9) + signet (~> 0.7) + highline (1.7.8) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.3.1) + json (2.1.0) + jwt (1.5.6) + little-plugger (1.1.4) + logging (2.2.2) + little-plugger (~> 1.1) + multi_json (~> 1.10) + memoist (0.16.0) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mini_magick (4.5.1) + multi_json (1.12.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + nanaimo (0.2.3) + os (0.9.6) + plist (3.3.0) + public_suffix (2.0.5) + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.0.2) + rouge (2.0.7) + rubyzip (1.2.1) + security (0.1.3) + signet (0.7.3) + addressable (~> 2.3) + faraday (~> 0.9) + jwt (~> 1.5) + multi_json (~> 1.10) + slack-notifier (1.5.1) + terminal-notifier (1.8.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + tty-screen (0.5.0) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.4) + unicode-display_width (1.3.0) + word_wrap (1.0.0) + xcodeproj (1.5.1) + CFPropertyList (~> 2.3.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.3) + xcpretty (0.2.8) + rouge (~> 2.0.7) + xcpretty-travis-formatter (0.0.4) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane + fastlane-plugin-aws_device_farm (>= 0.3.0) + +BUNDLED WITH + 1.15.3 diff --git a/appium/Makefile b/appium/Makefile new file mode 100644 index 0000000000..cc224c2f20 --- /dev/null +++ b/appium/Makefile @@ -0,0 +1,38 @@ +create-test-bundle: + rm test_bundle.zip | true + zip -r test_bundle.zip tests/test_ios.py wheelhouse/ requirements.txt conftest.py + +create-android-test-bundle: + rm test_bundle.zip | true + zip -r test_bundle.zip tests/test_android.py wheelhouse/ requirements.txt conftest.py + +install: + cd example; yarn install + +copy-local-files-to-example: + rm -rf example/node_modules/react-native-sentry/lib/* + find example/node_modules/react-native-sentry/ios -name 'RN*.[h|m]' -exec rm {} \; + rm -rf example/node_modules/react-native-sentry/ios/Sentry/Sources/Sentry/* + rm -rf example/node_modules/react-native-sentry/android/* + cp -r ../lib/* example/node_modules/react-native-sentry/lib/ + find ../ios -name 'RN*.[h|m]' -exec cp {} example/node_modules/react-native-sentry/ios/ \; + cp -r ../android/* example/node_modules/react-native-sentry/android/ + cp -r ../ios/Sentry/Sources/Sentry/* example/node_modules/react-native-sentry/ios/Sentry/Sources/Sentry/ + +test: create-test-bundle install copy-local-files-to-example + fastlane build_for_device_farm + fastlane aws_ios_upload_and_run + ruby check_run_failues.rb + +test-android: create-android-test-bundle install copy-local-files-to-example + fastlane build_android_for_device_farm + fastlane aws_android_upload_and_run + ANDROID=1 ruby check_run_failues.rb + +local-android-test: + fastlane build_android_for_device_farm + ANDROID=1 pytest -vv tests/test_android.py + +local-test: + fastlane build_for_local_appium + pytest -vv tests/test_ios.py diff --git a/appium/check_run_failues.rb b/appium/check_run_failues.rb new file mode 100644 index 0000000000..44272e0465 --- /dev/null +++ b/appium/check_run_failues.rb @@ -0,0 +1,79 @@ +require 'aws-sdk' +require 'open-uri' + +arn = File.read('./fastlane/.aws.run.arn') +arn.strip! + +@client = ::Aws::DeviceFarm::Client.new + +@problems = @client.list_unique_problems({ + arn: arn +}) + + +def android_check + @problems.unique_problems.each do |up| + raise RuntimeError, "No failed tests: #{up.inspect}" unless up.length == 2 + up[1].each do |p| + if p.problems[0].test.name == 'test_throw_error' + artifacts = @client.list_artifacts({ + type: "FILE", + arn: p.problems[0].test.arn + }) + artifacts.artifacts.each do |a| + if a.name == 'Logcat' + content = open(a.url).read + raise RuntimeError, "Missing value raven: #{p.inspect}" unless content.scan(/Raven about to send:/).size == 1 + raise RuntimeError, "Missing value: #{p.inspect}" unless content.scan(/value: 'Sentry: Test throw error'/).size == 1 + end + end + end + if p.problems[0].test.name == 'test_native_crash' + artifacts = @client.list_artifacts({ + type: "FILE", + arn: p.problems[0].test.arn + }) + artifacts.artifacts.each do |a| + if a.name == 'Logcat' + content = open(a.url).read + raise RuntimeError, "Missing native crash: #{p.inspect}" unless content.scan(/java.lang.RuntimeException: TEST - Sentry Client Crash/).size == 1 + end + end + end + end + end +end + +def ios_check + @problems.unique_problems.each do |up| + raise RuntimeError, "No failed tests: #{up.inspect}" unless up.length == 2 + up[1].each do |p| + if p.problems[0].test.name == 'test_throw_error' + artifacts = @client.list_artifacts({ + type: "FILE", + arn: p.problems[0].test.arn + }) + artifacts.artifacts.each do |a| + if a.name == 'Syslog' + content = open(a.url).read + raise RuntimeError, "No JSON SENT: #{p.inspect}" unless content.scan(/Sentry - Verbose:: Sending JSON/).size == 1 + raise RuntimeError, "Wrong exception value: #{p.inspect}" unless content.scan(/"value" : "Sentry: Test throw error"/).size == 1 + raise RuntimeError, "No javascript frames: #{p.inspect}" unless content.scan(/"platform" : "javascript"/).size >= 1 + end + end + end + if p.problems[0].test.name == 'test_native_crash' + exception = p.message.match(/crashed: EXC_/) + raise RuntimeError, "No crash: #{p.inspect}" unless !exception.nil? + exception = nil + end + end + end +end + + +if ENV['ANDROID'] == '1' + android_check +else + ios_check +end diff --git a/appium/conftest.py b/appium/conftest.py new file mode 100644 index 0000000000..3600ae7f9a --- /dev/null +++ b/appium/conftest.py @@ -0,0 +1,96 @@ +import os +import sys +import pytest +import traceback + +from appium import webdriver + +DEBUG_DRIVER = os.environ.get('DEBUG_DRIVER') == '1' + + +def hook_driver(driver): + real_execute = driver.command_executor.execute + def execute_proxy(*args, **kwargs): + print 'calling remote', args, kwargs + traceback.print_stack(file=sys.stdout, limit=4) + return real_execute(*args, **kwargs) + driver.command_executor.execute = execute_proxy + + +class DriverProxy(object): + + def __init__(self, make_driver): + self._make_driver = make_driver + self._driver = None + + def quit(self): + if self._driver is None: + return + + # this fails but actually succeeds + try: + self._driver.quit() + except Exception: + pass + self._driver = None + + def relaunch_app(self): + #self.quit() if we quit here, we loose connection to the app + + # this fails but actually succeeds + try: + self.launch_app() + except Exception: + pass + + def _get_driver(self): + if self._driver is None: + self._driver = self._make_driver() + if DEBUG_DRIVER: + hook_driver(self._driver) + return self._driver + + def __getattr__(self, name): + return getattr(self._get_driver(), name) + + +@pytest.fixture(scope='function') +def driver(request): + def make_driver(): + return webdriver.Remote( + command_executor='http://127.0.0.1:4723/wd/hub', + desired_capabilities=default_capabilities()) + + driver = DriverProxy(make_driver) + request.addfinalizer(driver.quit) + + return driver + + +@pytest.fixture(scope='function') +def on_aws(): + return runs_on_aws() + + +def runs_on_aws(): + return os.getenv('SCREENSHOT_PATH', False) + + +def default_capabilities(): + desired_caps = {} + + desired_caps['noReset'] = True + desired_caps['showIOSLog'] = True + if not runs_on_aws(): + if os.environ.get('ANDROID') == '1': + desired_caps['app'] = os.path.abspath('example/android/app/build/outputs/apk/app-release-unsigned.apk') + desired_caps['platformName'] = 'Android' + desired_caps['deviceName'] = 'Android' + else: + desired_caps['app'] = os.path.abspath('aws/Build/Products/Release-iphonesimulator/AwesomeProject.app') + desired_caps['platformName'] = 'iOS' + desired_caps['platformVersion'] = '10.3' + desired_caps['deviceName'] = 'iPhone Simulator' + + + return desired_caps diff --git a/appium/example b/appium/example new file mode 120000 index 0000000000..425f3b5787 --- /dev/null +++ b/appium/example @@ -0,0 +1 @@ +../examples/react-native/AwesomeProject \ No newline at end of file diff --git a/appium/fastlane/Appfile b/appium/fastlane/Appfile new file mode 100644 index 0000000000..f372081824 --- /dev/null +++ b/appium/fastlane/Appfile @@ -0,0 +1,7 @@ +app_identifier "org.reactjs.native.example.AwesomeProject" # The bundle identifier of your app +apple_id "" # Your Apple email address + +team_id "" # Developer Portal Team ID + +# you can even provide different app identifiers, Apple IDs and team names per lane: +# More information: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Appfile.md diff --git a/appium/fastlane/Fastfile b/appium/fastlane/Fastfile new file mode 100644 index 0000000000..92f9e5dabc --- /dev/null +++ b/appium/fastlane/Fastfile @@ -0,0 +1,93 @@ +fastlane_version "2.48.0" + +default_platform :ios + +platform :ios do + before_all do + # ENV["SLACK_URL"] = "https://hooks.slack.com/services/..." + + end + + lane :build_for_local_appium do + xcodebuild( + scheme: "AwesomeProject", + project: "example/ios/AwesomeProject.xcodeproj", + destination: "generic/platform=iOS Simulator", + configuration: "Release", + derivedDataPath: "aws", + xcargs: "GCC_PREPROCESSOR_DEFINITIONS='AWS_UI_TEST' ENABLE_BITCODE=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO build-for-testing" + ) + end + + lane :build_for_device_farm do + xcodebuild( + scheme: "AwesomeProject", + project: "example/ios/AwesomeProject.xcodeproj", + destination: "generic/platform=iOS", + configuration: "Release", + derivedDataPath: "aws", + xcargs: "GCC_PREPROCESSOR_DEFINITIONS='AWS_UI_TEST' ENABLE_BITCODE=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO build-for-testing" + ) + end + + lane :aws_ios_upload_and_run do + # ENV['AWS_ACCESS_KEY_ID'] = '' + # ENV['AWS_SECRET_ACCESS_KEY'] = '' + # ENV['AWS_REGION'] = 'us-west-2' + + # Transform .app into AWS compatible IPA + aws_device_farm_package( + derrived_data_path: "aws", + configuration: "Release" + ) + + # RUN tests on AWS Device Farm + aws_device_farm( + name: "react-native", + test_binary_path: "test_bundle.zip", + test_package_type: "APPIUM_PYTHON_TEST_PACKAGE", + test_type: 'APPIUM_PYTHON', + allow_failed_tests: true + ) + + store_run_arn + end + + lane :build_android_for_device_farm do + sh("cd ../example/android/; ./gradlew assembleRelease --stacktrace") + sh("jarsigner -verbose -digestalg SHA1 -sigalg MD5withRSA -keystore release.keystore -storepass 123456 ../example/android/app/build/outputs/apk/app-release-unsigned.apk release") + end + + lane :aws_android_upload_and_run do + aws_device_farm( + name: "react-native", + binary_path: "example/android/app/build/outputs/apk/app-release-unsigned.apk", + device_pool: "Android", + test_binary_path: "test_bundle.zip", + test_package_type: "APPIUM_PYTHON_TEST_PACKAGE", + test_type: 'APPIUM_PYTHON', + allow_failed_tests: true + ) + + store_run_arn + end + + lane :store_run_arn do + sh("echo #{ENV['AWS_DEVICE_FARM_RUN_ARN']} > .aws.run.arn") + end + + after_all do |lane| + # This block is called, only if the executed lane was successful + + # slack( + # message: "Successfully deployed new App Update." + # ) + end + + error do |lane, exception| + # slack( + # message: exception.message, + # success: false + # ) + end +end diff --git a/appium/fastlane/Pluginfile b/appium/fastlane/Pluginfile new file mode 100644 index 0000000000..49dd54635b --- /dev/null +++ b/appium/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-aws_device_farm', :git => 'git://github.com/HazAT/fastlane-plugin-aws_device_farm.git' diff --git a/appium/fastlane/release.keystore b/appium/fastlane/release.keystore new file mode 100644 index 0000000000..e7dd22b219 Binary files /dev/null and b/appium/fastlane/release.keystore differ diff --git a/appium/requirements.txt b/appium/requirements.txt new file mode 100644 index 0000000000..deb6f90c92 --- /dev/null +++ b/appium/requirements.txt @@ -0,0 +1,10 @@ +Appium-Python-Client==0.24 +pbr==3.1.1 +py==1.4.34 +pytest==3.1.3 +selenium==3.4.3 +six==1.10.0 +stevedore==1.24.0 +virtualenv==15.1.0 +virtualenv-clone==0.2.6 +virtualenvwrapper==4.7.2 diff --git a/appium/tests/test_android.py b/appium/tests/test_android.py new file mode 100644 index 0000000000..1782e241cb --- /dev/null +++ b/appium/tests/test_android.py @@ -0,0 +1,37 @@ +import os +import json + +from time import sleep + +def test_send_message(driver): + driver.find_element_by_accessibility_id('send message').click() + + sleep(3) + + value = driver.find_elements_by_xpath('//android.widget.EditText')[0].text + + assert value != None + event = json.loads(value) + + assert event['event_id'] != None + assert event['level'] == 'warning' + +def test_throw_error(driver): + driver.find_element_by_accessibility_id('throw error').click() + driver.relaunch_app() + value = driver.find_elements_by_xpath('//android.widget.EditText')[0].text + # the crash should have been already sent + assert value is None + +def test_native_crash(driver): + sleep(2) + driver.find_element_by_accessibility_id('native crash').click() + driver.relaunch_app() + sleep(3) + value = driver.find_elements_by_xpath('//android.widget.EditText')[0].text + + assert value != None + event = json.loads(value) + + assert event['event_id'] != None + assert event['level'] == 'fatal' diff --git a/appium/tests/test_ios.py b/appium/tests/test_ios.py new file mode 100644 index 0000000000..d4ff07bfba --- /dev/null +++ b/appium/tests/test_ios.py @@ -0,0 +1,94 @@ +import os +import json + +from time import sleep + + +def extractText(driver): + return driver.find_element_by_accessibility_id('textarea').get_attribute("value") + + +def test_send_message(driver): + driver.find_element_by_accessibility_id('send message').click() + sleep(3) + value = extractText(driver) + assert value != None + event = json.loads(value) + assert len(event['breadcrumbs']) > 0 + assert len(event['contexts']) > 0 + assert event['message'] == 'TEST message' + assert event['extra']['react'] + assert event['tags']['react'] == '1' + assert event['sdk']['integrations'][0] == 'react-native' + assert len(event['user']) > 0 + + +def test_version(driver): + driver.find_element_by_accessibility_id('set version').click() + driver.find_element_by_accessibility_id('send message').click() + sleep(3) + value = extractText(driver) + assert value != None + event = json.loads(value) + assert event['release'] == 'org.reactjs.native.example.AwesomeProject-1337' + + +def test_release(driver): + driver.find_element_by_accessibility_id('set release').click() + driver.find_element_by_accessibility_id('send message').click() + sleep(3) + value = extractText(driver) + assert value != None + event = json.loads(value) + assert event['release'] == 'myversion' + + +def test_dist(driver): + driver.find_element_by_accessibility_id('set dist').click() + driver.find_element_by_accessibility_id('send message').click() + sleep(3) + value = extractText(driver) + assert value != None + event = json.loads(value) + assert event['dist'] == '500' + + +def test_throw_error(driver): + driver.find_element_by_accessibility_id('throw error').click() + driver.relaunch_app() + value = extractText(driver) + # the crash should have been already sent + assert value is None + +def test_native_crash(driver): + sleep(2) + driver.find_element_by_accessibility_id('native crash').click() + driver.relaunch_app() + sleep(3) + value = extractText(driver) + # the crash should have been already sent + assert value != None + event = json.loads(value) + + assert len(event['breadcrumbs']) > 0 + assert len(event['contexts']) > 0 + assert len(event['threads']['values']) > 0 + for thread in event['threads']['values']: + if thread['crashed']: + assert len(thread['stacktrace']['frames']) > 0 + cocoa_frames = 0 + js_frames = 0 + for frame in thread['stacktrace']['frames']: + if frame.get('package', None): + cocoa_frames += 1 + if frame.get('platform', None) == 'javascript': + js_frames += 1 + assert cocoa_frames > 0 + assert js_frames > 0 # does not work in release build + assert len(event['exception']['values']) > 0 + assert len(event['debug_meta']['images']) > 0 + assert event['platform'] == 'cocoa' + assert event['level'] == 'fatal' + assert event['extra']['react'] + assert event['tags']['react'] == '1' + assert len(event['user']) > 0 diff --git a/examples b/examples new file mode 160000 index 0000000000..68fa357fd7 --- /dev/null +++ b/examples @@ -0,0 +1 @@ +Subproject commit 68fa357fd7d85ebf404936d53f3c98a4947fcffe diff --git a/ios/RNSentry.m b/ios/RNSentry.m index 86614b8986..6d490942e7 100644 --- a/ios/RNSentry.m +++ b/ios/RNSentry.m @@ -175,9 +175,7 @@ - (void)setReleaseVersionDist:(SentryEvent *)event { if (event.extra[@"__sentry_dist"]) { event.dist = [NSString stringWithFormat:@"%@", event.extra[@"__sentry_dist"]]; } - NSMutableDictionary *prevExtra = SentryClient.sharedClient.extra.mutableCopy; - [prevExtra setValue:@[@"react-native"] forKey:@"__sentry_sdk_integrations"]; - SentryClient.sharedClient.extra = prevExtra; + [event.extra setValue:@[@"react-native"] forKey:@"__sentry_sdk_integrations"]; } RCT_EXPORT_MODULE() @@ -193,12 +191,11 @@ - (void)setReleaseVersionDist:(SentryEvent *)event { dispatch_once(&onceStartToken, ^{ NSError *error = nil; SentryClient *client = [[SentryClient alloc] initWithDsn:dsnString didFailWithError:&error]; - [SentryClient setSharedClient:client]; - [SentryClient.sharedClient startCrashHandlerWithError:&error]; - if (error) { - [NSException raise:@"SentryReactNative" format:@"%@", error.localizedDescription]; - } - SentryClient.sharedClient.shouldSendEvent = ^BOOL(SentryEvent * _Nonnull event) { + client.beforeSerializeEvent = ^(SentryEvent * _Nonnull event) { + [self injectReactNativeFrames:event]; + [self setReleaseVersionDist:event]; + }; + client.shouldSendEvent = ^BOOL(SentryEvent * _Nonnull event) { // We don't want to send an event after startup that came from a NSException of react native // Because we sent it already before the app crashed. if (nil != event.exceptions.firstObject.type && @@ -208,10 +205,11 @@ - (void)setReleaseVersionDist:(SentryEvent *)event { } return YES; }; - SentryClient.sharedClient.beforeSerializeEvent = ^(SentryEvent * _Nonnull event) { - [self injectReactNativeFrames:event]; - [self setReleaseVersionDist:event]; - }; + [SentryClient setSharedClient:client]; + [SentryClient.sharedClient startCrashHandlerWithError:&error]; + if (error) { + [NSException raise:@"SentryReactNative" format:@"%@", error.localizedDescription]; + } }); } @@ -355,6 +353,7 @@ - (void)swizzleCallNativeModule:(Class)class { [self addExceptionToEvent:sentryEvent type:exception[@"type"] value:exception[@"value"] frames:frames]; } [SentryClient.sharedClient sendEvent:sentryEvent withCompletionHandler:NULL]; + [RNSentryEventEmitter emitStoredEvent]; } - (void)addExceptionToEvent:(SentryEvent *)event type:(NSString *)type value:(NSString *)value frames:(NSArray *)frames { diff --git a/ios/RNSentryEventEmitter.h b/ios/RNSentryEventEmitter.h index 292be816c7..9d2db914ed 100644 --- a/ios/RNSentryEventEmitter.h +++ b/ios/RNSentryEventEmitter.h @@ -11,6 +11,6 @@ @interface RNSentryEventEmitter : RCTEventEmitter -+ (void)successfullySentEventWithId:(NSString *)eventId; ++ (void)emitStoredEvent; @end diff --git a/ios/RNSentryEventEmitter.m b/ios/RNSentryEventEmitter.m index b96613665e..f94a4cb546 100644 --- a/ios/RNSentryEventEmitter.m +++ b/ios/RNSentryEventEmitter.m @@ -9,17 +9,21 @@ #import "RNSentryEventEmitter.h" NSString *const kEventSentSuccessfully = @"Sentry/eventSentSuccessfully"; +NSString *const kEventStored = @"Sentry/eventStored"; @implementation RNSentryEventEmitter RCT_EXPORT_MODULE(); - (NSDictionary *)constantsToExport { - return @{ @"EVENT_SENT_SUCCESSFULLY": kEventSentSuccessfully}; + return @{ + @"EVENT_SENT_SUCCESSFULLY": kEventSentSuccessfully, + @"EVENT_STORED": kEventStored + }; } - (NSArray *)supportedEvents { - return @[@"Sentry/eventSentSuccessfully"]; + return @[kEventSentSuccessfully, kEventStored]; } @@ -36,13 +40,13 @@ - (void)stopObserving { [[NSNotificationCenter defaultCenter] removeObserver:self]; } -+ (void)successfullySentEventWithId:(NSString *)eventId { - [self postNotificationName:kEventSentSuccessfully withPayload:eventId]; ++ (void)emitStoredEvent { + [self postNotificationName:kEventStored withPayload:@""]; } + (void)postNotificationName:(NSString *)name withPayload:(NSObject *)object { NSDictionary *payload = @{@"payload": object}; - + [[NSNotificationCenter defaultCenter] postNotificationName:name object:self userInfo:payload]; diff --git a/ios/Sentry b/ios/Sentry index 8387778705..08ecf30e5a 160000 --- a/ios/Sentry +++ b/ios/Sentry @@ -1 +1 @@ -Subproject commit 8387778705edb289156993c2beb2098e719ff449 +Subproject commit 08ecf30e5a448f62e07480ce6faed2006bd13bfa diff --git a/lib/Sentry.js b/lib/Sentry.js index 3f29f90d5c..de8e5d4816 100644 --- a/lib/Sentry.js +++ b/lib/Sentry.js @@ -36,6 +36,9 @@ export class Sentry { if (Sentry._eventSentSuccessfully) Sentry._eventSentSuccessfully(event); } ); + Sentry.eventEmitter.addListener(RNSentryEventEmitter.EVENT_STORED, () => { + if (Sentry._internalEventStored) Sentry._internalEventStored(); + }); } Sentry._ravenClient = new RavenClient(Sentry._dsn, Sentry.options); } @@ -180,4 +183,8 @@ export class Sentry { static _captureEvent(event) { if (Sentry.isNativeClientAvailable()) Sentry._nativeClient.captureEvent(event); } + + static _setInternalEventStored(callback) { + Sentry._internalEventStored = callback; + } } diff --git a/lib/raven-plugin.js b/lib/raven-plugin.js index c084c8a7be..96df55caac 100644 --- a/lib/raven-plugin.js +++ b/lib/raven-plugin.js @@ -19,6 +19,7 @@ */ 'use strict'; import {NativeModules} from 'react-native'; +import {Sentry} from './Sentry'; function wrappedCallback(callback) { function dataCallback(data, original) { @@ -64,10 +65,6 @@ function urlencode(obj) { function reactNativePlugin(Raven, options, internalDataCallback) { options = options || {}; - // react-native doesn't have a document, so can't use default Image - // transport - use XMLHttpRequest instead - Raven.setTransport(reactNativePlugin._transport); - // Use data callback to strip device-specific paths from stack traces Raven.setDataCallback( wrappedCallback(function(data) { @@ -78,33 +75,39 @@ function reactNativePlugin(Raven, options, internalDataCallback) { }) ); - // Check for a previously persisted payload, and report it. - reactNativePlugin._restorePayload().then(function(payload) { - options.onInitialize && options.onInitialize(payload); - if (!payload) return; - Raven._sendProcessedPayload(payload, function(error) { - if (error) return; // Try again next launch. - reactNativePlugin._clearPayload(); - }); - })['catch'](function() {}); + if (options.nativeClientAvailable && options.nativeClientAvailable === false) { + // react-native doesn't have a document, so can't use default Image + // transport - use XMLHttpRequest instead + Raven.setTransport(reactNativePlugin._transport); - Raven.setShouldSendCallback(function(data, originalCallback) { - if (!(FATAL_ERROR_KEY in data)) { - // not a fatal (will not crash runtime), continue as planned - return originalCallback ? originalCallback.call(this, data) : true; - } + // Check for a previously persisted payload, and report it. + reactNativePlugin._restorePayload().then(function(payload) { + options.onInitialize && options.onInitialize(payload); + if (!payload) return; + Raven._sendProcessedPayload(payload, function(error) { + if (error) return; // Try again next launch. + reactNativePlugin._clearPayload(); + }); + })['catch'](function() {}); - var origError = data[FATAL_ERROR_KEY]; - delete data[FATAL_ERROR_KEY]; + Raven.setShouldSendCallback(function(data, originalCallback) { + if (!(FATAL_ERROR_KEY in data)) { + // not a fatal (will not crash runtime), continue as planned + return originalCallback ? originalCallback.call(this, data) : true; + } - reactNativePlugin._persistPayload(data).then(function() { - defaultHandler(origError, true); - handlingFatal = false; // In case it isn't configured to crash. - return null; - })['catch'](function() {}); + var origError = data[FATAL_ERROR_KEY]; + delete data[FATAL_ERROR_KEY]; - return false; // Do not continue. - }); + reactNativePlugin._persistPayload(data).then(function() { + defaultHandler(origError, true); + handlingFatal = false; // In case it isn't configured to crash. + return null; + })['catch'](function() {}); + + return false; // Do not continue. + }); + } // Make sure that if multiple fatals occur, we only persist the first one. // @@ -135,7 +138,9 @@ function reactNativePlugin(Raven, options, internalDataCallback) { } Raven.captureException(error, captureOptions); // We always want to tunnel errors to the default handler - defaultHandler(error, isFatal); + Sentry._setInternalEventStored(() => { + defaultHandler(error, isFatal); + }); }); }