Skip to content

Commit

Permalink
Merge pull request #787 from rollbar/wj-scrub-network-telemetry
Browse files Browse the repository at this point in the history
Network telemetry improvements
  • Loading branch information
waltjones authored Oct 15, 2019
2 parents 7518c71 + a771e46 commit bf9a2ef
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 34 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"passwordConfirmation",
"access_token",
"accessToken",
"X-Rollbar-Access-Token",
"secret_key",
"secretKey",
"secretToken",
Expand Down
139 changes: 105 additions & 34 deletions src/browser/telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var defaults = {
network: true,
networkResponseHeaders: false,
networkResponseBody: false,
networkRequestHeaders: false,
networkRequestBody: false,
log: true,
dom: true,
Expand Down Expand Up @@ -58,6 +59,7 @@ function defaultValueScrubber(scrubFields) {
}

function Instrumenter(options, telemeter, rollbar, _window, _document) {
this.options = options;
var autoInstrument = options.autoInstrument;
if (options.enabled === false || autoInstrument === false) {
this.autoInstrument = {};
Expand Down Expand Up @@ -91,6 +93,7 @@ function Instrumenter(options, telemeter, rollbar, _window, _document) {
}

Instrumenter.prototype.configure = function(options) {
this.options = _.merge(this.options, options);
var autoInstrument = options.autoInstrument;
var oldSettings = _.merge(this.autoInstrument);
if (options.enabled === false || autoInstrument === false) {
Expand Down Expand Up @@ -169,6 +172,22 @@ Instrumenter.prototype.instrumentNetwork = function() {
start_time_ms: _.now(),
end_time_ms: null
};
if (self.autoInstrument.networkRequestHeaders) {
this.__rollbar_xhr.request_headers = {};
}
}
return orig.apply(this, arguments);
};
}, this.replacements, 'network');

replace(xhrp, 'setRequestHeader', function(orig) {
return function(header, value) {
if (self.autoInstrument.networkRequestHeaders && this.__rollbar_xhr &&
_.isType(header, 'string') && _.isType(value, 'string')) {
this.__rollbar_xhr.request_headers[header] = value;
}
if (header.toLowerCase() === 'content-type') {
this.__rollbar_xhr.request_content_type = value;
}
return orig.apply(this, arguments);
};
Expand All @@ -184,11 +203,10 @@ Instrumenter.prototype.instrumentNetwork = function() {
if (xhr.__rollbar_xhr) {
if (xhr.__rollbar_xhr.status_code === null) {
xhr.__rollbar_xhr.status_code = 0;
var requestData = null;
if (self.autoInstrument.networkRequestBody) {
requestData = data;
xhr.__rollbar_xhr.request = data;
}
xhr.__rollbar_event = self.telemeter.captureNetwork(xhr.__rollbar_xhr, 'xhr', undefined, requestData);
xhr.__rollbar_event = self.captureNetwork(xhr.__rollbar_xhr, 'xhr', undefined);
}
if (xhr.readyState < 2) {
xhr.__rollbar_xhr.start_time_ms = _.now();
Expand All @@ -197,6 +215,7 @@ Instrumenter.prototype.instrumentNetwork = function() {
xhr.__rollbar_xhr.end_time_ms = _.now();

var headers = null;
xhr.__rollbar_xhr.response_content_type = xhr.getResponseHeader('Content-Type');
if (self.autoInstrument.networkResponseHeaders) {
var headersConfig = self.autoInstrument.networkResponseHeaders;
headers = {};
Expand Down Expand Up @@ -237,7 +256,11 @@ Instrumenter.prototype.instrumentNetwork = function() {
if (body || headers) {
response = {};
if (body) {
response.body = body;
if (self.isJsonContentType(xhr.__rollbar_xhr.request_content_type)) {
response.body = self.scrubJson(body);
} else {
response.body = body;
}
}
if (headers) {
response.headers = headers;
Expand Down Expand Up @@ -304,49 +327,57 @@ Instrumenter.prototype.instrumentNetwork = function() {
start_time_ms: _.now(),
end_time_ms: null
};
var requestData = null;
if (args[1] && args[1].headers) {
// Argument may be a Headers object, or plain object. Ensure here that
// we are working with a Headers object with case-insensitive keys.
var reqHeaders = new Headers(args[1].headers);

metadata.request_content_type = reqHeaders.get('Content-Type');

if (self.autoInstrument.networkRequestHeaders) {
metadata.request_headers = self.fetchHeaders(reqHeaders, self.autoInstrument.networkRequestHeaders)
}
}

if (self.autoInstrument.networkRequestBody) {
if (args[1] && args[1].body) {
requestData = args[1].body;
metadata.request = args[1].body;
} else if (args[0] && !_.isType(args[0], 'string') && args[0].body) {
requestData = args[0].body;
metadata.request = args[0].body;
}
}
self.telemeter.captureNetwork(metadata, 'fetch', undefined, requestData);
self.captureNetwork(metadata, 'fetch', undefined);
return orig.apply(this, args).then(function (resp) {
metadata.end_time_ms = _.now();
metadata.status_code = resp.status;
metadata.response_content_type = resp.headers.get('Content-Type');
var headers = null;
if (self.autoInstrument.networkResponseHeaders) {
var headersConfig = self.autoInstrument.networkResponseHeaders;
headers = {};
try {
if (headersConfig === true) {
// This is unsupported in IE so we can't do it
/*
var allHeaders = resp.headers.entries();
for (var pair of allHeaders) {
headers[pair[0]] = pair[1];
}
*/
headers = self.fetchHeaders(resp.headers, self.autoInstrument.networkResponseHeaders);
}
var body = null;
if (self.autoInstrument.networkResponseBody) {
if (typeof resp.text === 'function') { // Response.text() is not implemented on multiple platforms
body = resp.text(); //returns a Promise
}
}
if (headers || body) {
metadata.response = {};
if (body) {
// Test to ensure body is a Promise, which it should always be.
if (typeof body.then === 'function') {
body.then(function (text) {
if (self.isJsonContentType(metadata.response_content_type)) {
metadata.response.body = self.scrubJson(text);
}
});
} else {
for (var i=0; i < headersConfig.length; i++) {
var header = headersConfig[i];
headers[header] = resp.headers.get(header);
}
metadata.response.body = body;
}
} catch (e) {
/* ignore probable IE errors */
}
}
var response = null;
if (headers) {
response = {
headers: headers
};
}
if (response) {
metadata.response = response;
if (headers) {
metadata.response.headers = headers;
}
}
return resp;
});
Expand All @@ -355,6 +386,46 @@ Instrumenter.prototype.instrumentNetwork = function() {
}
};

Instrumenter.prototype.captureNetwork = function(metadata, subtype, rollbarUUID) {
if (metadata.request && this.isJsonContentType(metadata.request_content_type)) {
metadata.request = this.scrubJson(metadata.request);
}
return this.telemeter.captureNetwork(metadata, subtype, rollbarUUID);
};

Instrumenter.prototype.isJsonContentType = function(contentType) {
return (contentType && contentType.toLowerCase().includes('json')) ? true : false;
}

Instrumenter.prototype.scrubJson = function(json) {
return JSON.stringify(_.scrub(JSON.parse(json), this.options.scrubFields));
}

Instrumenter.prototype.fetchHeaders = function(inHeaders, headersConfig) {
var outHeaders = {};
try {
var i;
if (headersConfig === true) {
if (typeof inHeaders.entries === 'function') { // Headers.entries() is not implemented in IE
var allHeaders = inHeaders.entries();
var currentHeader = allHeaders.next();
while (!currentHeader.done) {
outHeaders[currentHeader.value[0]] = currentHeader.value[1];
currentHeader = allHeaders.next();
}
}
} else {
for (i=0; i < headersConfig.length; i++) {
var header = headersConfig[i];
outHeaders[header] = inHeaders.get(header);
}
}
} catch (e) {
/* ignore probable IE errors */
}
return outHeaders;
}

Instrumenter.prototype.deinstrumentConsole = function() {
if (!('console' in this._window && this._window.console.log)) {
return;
Expand Down
127 changes: 127 additions & 0 deletions test/browser.rollbar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,133 @@ describe('options.autoInstrument.log', function() {
done();
});

it('should add telemetry events for xhr calls', function(done) {
var server = window.server;
stubResponse(server);
server.requests.length = 0;

server.respondWith('POST', 'xhr-test',
[
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({name: 'foo', password: '123456'})
]
);

var options = {
accessToken: 'POST_CLIENT_ITEM_TOKEN',
autoInstrument: {
log: false,
network: true,
networkResponseHeaders: true,
networkResponseBody: true,
networkRequestBody: true,
networkRequestHeaders: true
}
};
var rollbar = window.rollbar = new Rollbar(options);

// generate a telemetry event
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://example.com/xhr-test', true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.setRequestHeader('Secret', 'abcdef');
xhr.onreadystatechange = function () {
if(xhr.readyState === 4) {
try {
rollbar.log('test'); // generate a payload to inspect

expect(server.requests.length).to.eql(2);
var body = JSON.parse(server.requests[1].requestBody);

// Sinon fake server doesn't generate response body or headers that are visible
// via the XMLHttpRequest properties and methods. So we settle for verifying
// the request only.

// Verify request capture and scrubbing
expect(body.data.body.telemetry[0].body.request).to.eql('{"name":"bar","secret":"********"}');

// Verify request headers capture and case-insensitive scrubbing
expect(body.data.body.telemetry[0].body.request_headers).to.eql({'Content-type': 'application/json', Secret: '********'});

done();
} catch (e) {
done(e);
}
}
};
xhr.send(JSON.stringify({name: 'bar', secret: 'xhr post' }));
server.respond();
});

it('should add telemetry events for fetch calls', function(done) {
var server = window.server;
stubResponse(server);
server.requests.length = 0;

window.fetchStub = sinon.stub(window, 'fetch');
window.fetch.returns(Promise.resolve(new Response(
JSON.stringify({name: 'foo', password: '123456'}),
{ status: 200, statusText: 'OK', headers: { 'content-type': 'application/json', 'password': '123456' }}
)));

var options = {
accessToken: 'POST_CLIENT_ITEM_TOKEN',
autoInstrument: {
log: false,
network: true,
networkResponseHeaders: true,
networkResponseBody: true,
networkRequestBody: true,
networkRequestHeaders: true
}
};
var rollbar = window.rollbar = new Rollbar(options);

var fetchHeaders = new Headers();
fetchHeaders.append('Content-Type', 'application/json');
fetchHeaders.append('Secret', '123456');

const fetchInit = {
method: 'POST',
headers: fetchHeaders,
body: JSON.stringify({name: 'bar', secret: 'xhr post'})
};
var fetchRequest = new Request('https://example.com/xhr-test');
window,fetch(fetchRequest, fetchInit)
.then(function(_response) {
try {
rollbar.log('test'); // generate a payload to inspect
server.respond();

expect(server.requests.length).to.eql(1);
var body = JSON.parse(server.requests[0].requestBody);

// Verify request capture and scrubbing
expect(body.data.body.telemetry[0].body.request).to.eql('{"name":"bar","secret":"********"}');

// Verify request headers capture and case-insensitive scrubbing
expect(body.data.body.telemetry[0].body.request_headers).to.eql({'content-type': 'application/json', secret: '********'});

// When using the Sinon test stub, the response body is populated in Headless Chrome 73,
// but not in 77. When using the Fetch API normally, it is populated in all tested Chrome versions.
// Disable here due to the Sinon limitation.
//
// Verify response capture and scrubbing
// expect(body.data.body.telemetry[0].body.response.body).to.eql('{"name":"foo","password":"********"}');

// Verify response headers capture and case-insensitive scrubbing
expect(body.data.body.telemetry[0].body.response.headers).to.eql({'content-type': 'application/json', password: '********'});

rollbar.configure({ autoInstrument: false });
window.fetch.restore();
done();
} catch (e) {
done(e);
}
})
});

it('should add a diagnostic message when wrapConsole fails', function(done) {
var server = window.server;
stubResponse(server);
Expand Down

0 comments on commit bf9a2ef

Please sign in to comment.