Skip to content

Commit 3795d3e

Browse files
committed
Proof-of-concept using pako to compress request data with zlib, and set Content-Type/Content-Encoding headers. (Doesn't work though.)
1 parent 1ee72bf commit 3795d3e

File tree

2 files changed

+217
-14
lines changed

2 files changed

+217
-14
lines changed

index.js

Lines changed: 213 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,199 @@
11
const stringify = require("./vendor/json-stringify-safe/stringify");
2+
const pako = require('pako');
3+
4+
// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785)
5+
const _window =
6+
typeof window !== 'undefined'
7+
? window
8+
: typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
9+
10+
/**
11+
* hasKey, a better form of hasOwnProperty
12+
* Example: hasKey(MainHostObject, property) === true/false
13+
*
14+
* @param {Object} host object to check property
15+
* @param {string} key to check
16+
*/
17+
function hasKey(object, key) {
18+
return Object.prototype.hasOwnProperty.call(object, key);
19+
}
20+
21+
function isUndefined(what) {
22+
return what === void 0;
23+
}
24+
25+
function each(obj, callback) {
26+
var i, j;
27+
28+
if (isUndefined(obj.length)) {
29+
for (i in obj) {
30+
if (hasKey(obj, i)) {
31+
callback.call(null, i, obj[i]);
32+
}
33+
}
34+
} else {
35+
j = obj.length;
36+
if (j) {
37+
for (i = 0; i < j; i++) {
38+
callback.call(null, i, obj[i]);
39+
}
40+
}
41+
}
42+
}
43+
44+
function supportsFetch() {
45+
if (!('fetch' in _window)) return false;
46+
47+
try {
48+
new Headers(); // eslint-disable-line no-new
49+
new Request(''); // eslint-disable-line no-new
50+
new Response(); // eslint-disable-line no-new
51+
return true;
52+
} catch (e) {
53+
return false;
54+
}
55+
}
56+
57+
function urlencode(o) {
58+
var pairs = [];
59+
each(o, function(key, value) {
60+
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
61+
});
62+
return pairs.join('&');
63+
}
64+
65+
function objectMerge(obj1, obj2) {
66+
if (!obj2) {
67+
return obj1;
68+
}
69+
each(obj2, function(key, value) {
70+
obj1[key] = value;
71+
});
72+
return obj1;
73+
}
74+
75+
76+
// Unfortunately, this doesn't work at the moment, because Sentry doesn't allow us to
77+
// send a Content-Encoding header. The CORS preflight request returns:
78+
//
79+
// Access-Control-Allow-Headers: X-Sentry-Auth, X-Requested-With,
80+
// Origin, Accept, Content-Type, Authentication
81+
//
82+
// (Content-Encoding is missing.)
83+
//
84+
// Also it might be dangerous for them to support gzip/deflate, because people could
85+
// send gzip bombs (a tiny amount of compressed data that expands to gigabytes on disk)
86+
// But maybe there's a decompression library that can be configured to abort after a max size.
87+
88+
const makeRequestWithZlib = function(opts, shouldSendRequest) {
89+
// Auth is intentionally sent as part of query string (NOT as custom HTTP header) to avoid preflight CORS requests
90+
var url = opts.url + '?' + urlencode(opts.auth);
91+
92+
var evaluatedHeaders = null;
93+
var evaluatedFetchParameters = {};
94+
95+
if (opts.options.headers) {
96+
evaluatedHeaders = this._evaluateHash(opts.options.headers);
97+
}
98+
99+
if (opts.options.fetchParameters) {
100+
evaluatedFetchParameters = this._evaluateHash(opts.options.fetchParameters);
101+
}
102+
103+
const requestJSON = stringify(opts.data);
104+
const requestDeflate = pako.deflate(requestJSON, { to: 'string' });
105+
106+
if (shouldSendRequest && !shouldSendRequest(requestDeflate)) {
107+
return;
108+
}
109+
110+
if (supportsFetch()) {
111+
var defaultFetchOptions = objectMerge({}, this._fetchDefaults);
112+
var fetchOptions = objectMerge(defaultFetchOptions, evaluatedFetchParameters);
113+
114+
if (evaluatedHeaders) {
115+
fetchOptions.headers = evaluatedHeaders;
116+
}
117+
118+
evaluatedFetchParameters.body = requestDeflate
119+
120+
fetchOptions.headers = Object.assign(fetchOptions.headers || {}, {
121+
'Content-Type': 'application/json; charset=utf-8',
122+
'Content-Encoding': 'deflate',
123+
})
124+
125+
return _window
126+
.fetch(url, fetchOptions)
127+
.then(function(response) {
128+
if (response.ok) {
129+
opts.onSuccess && opts.onSuccess();
130+
} else {
131+
var error = new Error('Sentry error code: ' + response.status);
132+
// It's called request only to keep compatibility with XHR interface
133+
// and not add more redundant checks in setBackoffState method
134+
error.request = response;
135+
opts.onError && opts.onError(error);
136+
}
137+
})
138+
['catch'](function() {
139+
opts.onError &&
140+
opts.onError(new Error('Sentry error code: network unavailable'));
141+
});
142+
}
143+
144+
var request = _window.XMLHttpRequest && new _window.XMLHttpRequest();
145+
if (!request) return;
146+
147+
// if browser doesn't support CORS (e.g. IE7), we are out of luck
148+
var hasCORS = 'withCredentials' in request || typeof XDomainRequest !== 'undefined';
149+
150+
if (!hasCORS) return;
151+
152+
if ('withCredentials' in request) {
153+
request.onreadystatechange = function() {
154+
if (request.readyState !== 4) {
155+
return;
156+
} else if (request.status === 200) {
157+
opts.onSuccess && opts.onSuccess();
158+
} else if (opts.onError) {
159+
var err = new Error('Sentry error code: ' + request.status);
160+
err.request = request;
161+
opts.onError(err);
162+
}
163+
};
164+
} else {
165+
request = new XDomainRequest();
166+
// xdomainrequest cannot go http -> https (or vice versa),
167+
// so always use protocol relative
168+
url = url.replace(/^https?:/, '');
169+
170+
// onreadystatechange not supported by XDomainRequest
171+
if (opts.onSuccess) {
172+
request.onload = opts.onSuccess;
173+
}
174+
if (opts.onError) {
175+
request.onerror = function() {
176+
var err = new Error('Sentry error code: XDomainRequest');
177+
err.request = request;
178+
opts.onError(err);
179+
};
180+
}
181+
}
182+
183+
request.open('POST', url);
184+
185+
if (evaluatedHeaders) {
186+
each(evaluatedHeaders, function(key, value) {
187+
request.setRequestHeader(key, value);
188+
});
189+
}
190+
191+
request.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
192+
request.setRequestHeader('Content-Encoding', 'deflate');
193+
194+
request.send(requestDeflate);
195+
}
196+
2197

3198
const identity = x => x;
4199
const getUndefined = () => {};
@@ -57,18 +252,6 @@ function createRavenMiddleware(Raven, options = {}) {
57252
const originalTransport = Raven._globalOptions.transport;
58253
Raven.setTransport(opts => {
59254
Raven.setTransport(originalTransport);
60-
const requestBody = stringify(opts.data);
61-
if (requestBody.length > 200000) {
62-
// We know the request is too large, so don't try sending it to Sentry.
63-
// Retry the capture function, and don't include the state this time.
64-
const errorMessage =
65-
"Could not send state because request would be larger than 200KB. " +
66-
`(Was: ${requestBody.length}B)`;
67-
retryCaptureWithoutReduxState(errorMessage, () => {
68-
originalFn.apply(Raven, captureArguments);
69-
});
70-
return;
71-
}
72255
opts.onError = error => {
73256
if (error.request && error.request.status === 413) {
74257
const errorMessage =
@@ -78,7 +261,21 @@ function createRavenMiddleware(Raven, options = {}) {
78261
});
79262
}
80263
};
81-
(originalTransport || Raven._makeRequest).call(Raven, opts);
264+
265+
makeRequestWithZlib(opts, (requestBody) => {
266+
if (requestBody.length > 200000) {
267+
// We know the request is too large, so don't try sending it to Sentry.
268+
// Retry the capture function, and don't include the state this time.
269+
const errorMessage =
270+
"Could not send state because request would be larger than 200KB. " +
271+
`(Was: ${requestBody.length}B)`;
272+
retryCaptureWithoutReduxState(errorMessage, () => {
273+
originalFn.apply(Raven, captureArguments);
274+
});
275+
return false;
276+
}
277+
return true;
278+
});
82279
});
83280
originalFn.apply(Raven, captureArguments);
84281
};
@@ -91,6 +288,9 @@ function createRavenMiddleware(Raven, options = {}) {
91288
Raven.captureMessage
92289
);
93290

291+
// Set the default transport to use zlib compression
292+
Raven.setTransport(makeRequestWithZlib.bind(Raven));
293+
94294
return next => action => {
95295
// Log the action taken to Raven so that we have narrative context in our
96296
// error report.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "raven-for-redux",
33
"version": "1.3.1",
44
"description": "Middleware for propagating Redux state/actions to Sentry via Raven.",
5-
"main": "built/index.js",
5+
"main": "index.js",
66
"scripts": {
77
"fix": "eslint --fix .",
88
"tdd": "jest --watch",
@@ -64,5 +64,8 @@
6464
"statements": 100
6565
}
6666
}
67+
},
68+
"dependencies": {
69+
"pako": "^1.0.6"
6770
}
6871
}

0 commit comments

Comments
 (0)