Skip to content

Commit 639131b

Browse files
committed
Merge pull request #531 from getsentry/react-native-tests
Add tests for React Native plugin
2 parents 29b52da + 2f271af commit 639131b

File tree

2 files changed

+224
-55
lines changed

2 files changed

+224
-55
lines changed

plugins/react-native.js

Lines changed: 74 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,80 +10,99 @@
1010
'use strict';
1111

1212
var DEVICE_PATH_RE = /^\/var\/mobile\/Containers\/Bundle\/Application\/[^\/]+\/[^\.]+\.app/;
13+
14+
/**
15+
* Strip device-specific IDs from React Native file:// paths
16+
*/
1317
function normalizeUrl(url) {
1418
return url
1519
.replace(/^file\:\/\//, '')
1620
.replace(DEVICE_PATH_RE, '');
1721
}
1822

19-
function reactNativePlugin(Raven) {
20-
function urlencode(obj) {
21-
var pairs = [];
22-
for (var key in obj) {
23-
if ({}.hasOwnProperty.call(obj, key))
24-
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
25-
}
26-
return pairs.join('&');
27-
}
28-
29-
function xhrTransport(options) {
30-
var request = new XMLHttpRequest();
31-
request.onreadystatechange = function (e) {
32-
if (request.readyState !== 4) {
33-
return;
34-
}
35-
36-
if (request.status === 200) {
37-
if (options.onSuccess) {
38-
options.onSuccess();
39-
}
40-
} else {
41-
if (options.onError) {
42-
options.onError();
43-
}
44-
}
45-
};
46-
47-
request.open('POST', options.url + '?' + urlencode(options.auth));
48-
49-
// NOTE: React Native ignores CORS and will NOT send a preflight
50-
// request for application/json.
51-
// See: https://facebook.github.io/react-native/docs/network.html#xmlhttprequest
52-
request.setRequestHeader('Content-type', 'application/json');
53-
54-
// Sentry expects an Origin header when using HTTP POST w/ public DSN.
55-
// Just set a phony Origin value; only matters if Sentry Project is configured
56-
// to whitelist specific origins.
57-
request.setRequestHeader('Origin', 'react-native://');
58-
request.send(JSON.stringify(options.data));
23+
/**
24+
* Extract key/value pairs from an object and encode them for
25+
* use in a query string
26+
*/
27+
function urlencode(obj) {
28+
var pairs = [];
29+
for (var key in obj) {
30+
if ({}.hasOwnProperty.call(obj, key))
31+
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
5932
}
33+
return pairs.join('&');
34+
}
6035

36+
/**
37+
* Initializes React Native plugin
38+
*/
39+
function reactNativePlugin(Raven) {
6140
// react-native doesn't have a document, so can't use default Image
6241
// transport - use XMLHttpRequest instead
63-
Raven.setTransport(xhrTransport);
64-
42+
Raven.setTransport(reactNativePlugin._transport);
6543

6644
// Use data callback to strip device-specific paths from stack traces
67-
Raven.setDataCallback(function (data) {
68-
if (data.culprit) {
69-
data.culprit = normalizeUrl(data.culprit);
70-
}
71-
72-
if (data.exception) {
73-
// if data.exception exists, all of the other keys are guaranteed to exist
74-
data.exception.values[0].stacktrace.frames.forEach(function (frame) {
75-
frame.filename = normalizeUrl(frame.filename);
76-
});
77-
}
78-
});
45+
Raven.setDataCallback(reactNativePlugin._normalizeData);
7946

8047
var defaultHandler = ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler() || ErrorUtils._globalHandler;
8148

82-
ErrorUtils.setGlobalHandler(function(){
49+
ErrorUtils.setGlobalHandler(function() {
8350
var error = arguments[0];
8451
defaultHandler.apply(this, arguments)
8552
Raven.captureException(error);
8653
});
8754
}
8855

56+
/**
57+
* Custom HTTP transport for use with React Native applications.
58+
*/
59+
reactNativePlugin._transport = function (options) {
60+
var request = new XMLHttpRequest();
61+
request.onreadystatechange = function (e) {
62+
if (request.readyState !== 4) {
63+
return;
64+
}
65+
66+
if (request.status === 200) {
67+
if (options.onSuccess) {
68+
options.onSuccess();
69+
}
70+
} else {
71+
if (options.onError) {
72+
options.onError();
73+
}
74+
}
75+
};
76+
77+
request.open('POST', options.url + '?' + urlencode(options.auth));
78+
79+
// NOTE: React Native ignores CORS and will NOT send a preflight
80+
// request for application/json.
81+
// See: https://facebook.github.io/react-native/docs/network.html#xmlhttprequest
82+
request.setRequestHeader('Content-type', 'application/json');
83+
84+
// Sentry expects an Origin header when using HTTP POST w/ public DSN.
85+
// Just set a phony Origin value; only matters if Sentry Project is configured
86+
// to whitelist specific origins.
87+
request.setRequestHeader('Origin', 'react-native://');
88+
request.send(JSON.stringify(options.data));
89+
};
90+
91+
/**
92+
* Strip device-specific IDs found in culprit and frame filenames
93+
* when running React Native applications on a physical device.
94+
*/
95+
reactNativePlugin._normalizeData = function (data) {
96+
if (data.culprit) {
97+
data.culprit = normalizeUrl(data.culprit);
98+
}
99+
100+
if (data.exception) {
101+
// if data.exception exists, all of the other keys are guaranteed to exist
102+
data.exception.values[0].stacktrace.frames.forEach(function (frame) {
103+
frame.filename = normalizeUrl(frame.filename);
104+
});
105+
}
106+
};
107+
89108
module.exports = reactNativePlugin;

test/plugins/react-native.test.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
var _Raven = require('../../src/raven');
2+
var reactNativePlugin = require('../../plugins/react-native');
3+
4+
window.ErrorUtils = {};
5+
6+
var Raven;
7+
describe('React Native plugin', function () {
8+
beforeEach(function () {
9+
Raven = new _Raven();
10+
Raven.config('http://[email protected]:80/2');
11+
});
12+
13+
describe('_normalizeData()', function () {
14+
it('should normalize culprit and frame filenames/URLs', function () {
15+
var data = {
16+
project: '2',
17+
logger: 'javascript',
18+
platform: 'javascript',
19+
20+
culprit: 'file:///var/mobile/Containers/Bundle/Application/ABC/123.app/app.js',
21+
message: 'Error: crap',
22+
exception: {
23+
type: 'Error',
24+
values: [{
25+
stacktrace: {
26+
frames: [{
27+
filename: 'file:///var/mobile/Containers/Bundle/Application/ABC/123.app/file1.js',
28+
lineno: 10,
29+
colno: 11,
30+
'function': 'broken'
31+
32+
}, {
33+
filename: 'file:///var/mobile/Containers/Bundle/Application/ABC/123.app/file2.js',
34+
lineno: 12,
35+
colno: 13,
36+
'function': 'lol'
37+
}]
38+
}
39+
}],
40+
}
41+
};
42+
reactNativePlugin._normalizeData(data);
43+
44+
assert.equal(data.culprit, '/app.js');
45+
var frames = data.exception.values[0].stacktrace.frames;
46+
assert.equal(frames[0].filename, '/file1.js');
47+
assert.equal(frames[1].filename, '/file2.js');
48+
});
49+
});
50+
51+
describe('_transport()', function () {
52+
beforeEach(function () {
53+
this.xhr = sinon.useFakeXMLHttpRequest();
54+
var requests = this.requests = [];
55+
56+
this.xhr.onCreate = function (xhr) {
57+
requests.push(xhr);
58+
};
59+
});
60+
61+
afterEach(function () {
62+
this.xhr.restore();
63+
});
64+
65+
it('should open and send a new XHR POST with urlencoded auth, fake origin', function () {
66+
reactNativePlugin._transport({
67+
url: 'https://example.org/1',
68+
auth: {
69+
sentry_version: '7',
70+
sentry_client: 'raven-js/2.2.0',
71+
sentry_key: 'abc123'
72+
},
73+
data: {foo: 'bar'}
74+
});
75+
76+
var lastXhr = this.requests.shift();
77+
lastXhr.respond(200);
78+
79+
assert.equal(
80+
lastXhr.url,
81+
'https://example.org/1?sentry_version=7&sentry_client=raven-js%2F2.2.0&sentry_key=abc123'
82+
);
83+
assert.equal(lastXhr.method, 'POST');
84+
assert.equal(lastXhr.requestBody, '{"foo":"bar"}');
85+
assert.equal(lastXhr.requestHeaders['Content-type'], 'application/json');
86+
assert.equal(lastXhr.requestHeaders['Origin'], 'react-native://');
87+
});
88+
89+
it('should call onError callback on failure', function () {
90+
var onError = this.sinon.stub();
91+
var onSuccess = this.sinon.stub();
92+
reactNativePlugin._transport({
93+
url: 'https://example.org/1',
94+
auth: {},
95+
data: {foo: 'bar'},
96+
onError: onError,
97+
onSuccess: onSuccess
98+
});
99+
100+
var lastXhr = this.requests.shift();
101+
lastXhr.respond(401);
102+
103+
assert.isTrue(onError.calledOnce);
104+
assert.isFalse(onSuccess.calledOnce);
105+
});
106+
107+
it('should call onSuccess callback on success', function () {
108+
var onError = this.sinon.stub();
109+
var onSuccess = this.sinon.stub();
110+
reactNativePlugin._transport({
111+
url: 'https://example.org/1',
112+
auth: {},
113+
data: {foo: 'bar'},
114+
onError: onError,
115+
onSuccess: onSuccess
116+
});
117+
118+
var lastXhr = this.requests.shift();
119+
lastXhr.respond(200);
120+
121+
assert.isTrue(onSuccess.calledOnce);
122+
assert.isFalse(onError.calledOnce);
123+
});
124+
});
125+
126+
describe('ErrorUtils global error handler', function () {
127+
beforeEach(function () {
128+
var self = this;
129+
ErrorUtils.setGlobalHandler = function(fn) {
130+
self.globalErrorHandler = fn;
131+
};
132+
self.defaultErrorHandler = self.sinon.stub();
133+
ErrorUtils.getGlobalHandler = function () {
134+
return self.defaultErrorHandler;
135+
}
136+
});
137+
138+
it('should call the default React Native handler and Raven.captureException', function () {
139+
reactNativePlugin(Raven);
140+
var err = new Error();
141+
this.sinon.stub(Raven, 'captureException');
142+
143+
this.globalErrorHandler(err);
144+
145+
assert.isTrue(this.defaultErrorHandler.calledOnce);
146+
assert.isTrue(Raven.captureException.calledOnce);
147+
assert.equal(Raven.captureException.getCall(0).args[0], err);
148+
});
149+
});
150+
});

0 commit comments

Comments
 (0)