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
+[](https://travis-ci.org/getsentry/react-native-sentry)
[](https://www.npmjs.com/package/react-native-sentry)
[](https://www.npmjs.com/package/react-native-sentry)
[](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);
+ });
});
}