Skip to content

Commit

Permalink
Add Stripe client telemetry to request headers (#557)
Browse files Browse the repository at this point in the history
  • Loading branch information
jameshageman-stripe authored and brandur-stripe committed Jan 31, 2019
1 parent 00fcbcb commit bb2acf9
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 2 deletions.
33 changes: 32 additions & 1 deletion lib/StripeResource.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ StripeResource.extend = utils.protoExtend;
StripeResource.method = require('./StripeMethod');
StripeResource.BASIC_METHODS = require('./StripeMethod.basic');

StripeResource.MAX_BUFFERED_REQUEST_METRICS = 100;

/**
* Encapsulates request logic for a Stripe Resource
*/
Expand Down Expand Up @@ -125,6 +127,8 @@ StripeResource.prototype = {
// lastResponse.
res.requestId = headers['request-id'];

var requestDurationMs = Date.now() - req._requestStart;

var responseEvent = utils.removeEmpty({
api_version: headers['stripe-version'],
account: headers['stripe-account'],
Expand All @@ -133,7 +137,7 @@ StripeResource.prototype = {
path: req._requestEvent.path,
status: res.statusCode,
request_id: res.requestId,
elapsed: Date.now() - req._requestStart,
elapsed: requestDurationMs,
});

self._stripe._emitter.emit('response', responseEvent);
Expand Down Expand Up @@ -171,6 +175,9 @@ StripeResource.prototype = {
null
);
}

self._recordRequestMetrics(res.requestId, requestDurationMs);

// Expose res object
Object.defineProperty(response, 'lastResponse', {
enumerable: false,
Expand Down Expand Up @@ -225,6 +232,28 @@ StripeResource.prototype = {
return headers;
},

_addTelemetryHeader: function(headers) {
if (this._stripe.getTelemetryEnabled() && this._stripe._prevRequestMetrics.length > 0) {
var metrics = this._stripe._prevRequestMetrics.shift();
headers['X-Stripe-Client-Telemetry'] = JSON.stringify({
'last_request_metrics': metrics
});
}
},

_recordRequestMetrics: function(requestId, requestDurationMs) {
if (this._stripe.getTelemetryEnabled() && requestId) {
if (this._stripe._prevRequestMetrics.length > StripeResource.MAX_BUFFERED_REQUEST_METRICS) {
utils.emitWarning('Request metrics buffer is full, dropping telemetry message.');
} else {
this._stripe._prevRequestMetrics.push({
'request_id': requestId,
'request_duration_ms': requestDurationMs,
});
}
}
},

_request: function(method, host, path, data, auth, options, callback) {
var self = this;
var requestData;
Expand All @@ -248,6 +277,8 @@ StripeResource.prototype = {
Object.assign(headers, options.headers);
}

self._addTelemetryHeader(headers);

makeRequest(apiVersion, headers);
});
}
Expand Down
12 changes: 11 additions & 1 deletion lib/stripe.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ function Stripe(key, version) {

this.errors = require('./Error');
this.webhooks = require('./Webhooks');

this._prevRequestMetrics = [];
this.setTelemetryEnabled(false);
}

Stripe.errors = require('./Error');
Expand Down Expand Up @@ -299,12 +302,19 @@ Stripe.prototype = {
return formatted;
},

setTelemetryEnabled: function(enableTelemetry) {
this._enableTelemetry = enableTelemetry;
},

getTelemetryEnabled: function() {
return this._enableTelemetry;
},

_prepResources: function() {
for (var name in resources) {
this[utils.pascalToCamelCase(name)] = new resources[name](this);
}
},

};

module.exports = Stripe;
Expand Down
2 changes: 2 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ var utils = module.exports = {
return name[0].toLowerCase() + name.substring(1);
}
},

emitWarning: emitWarning,
};

function emitWarning(warning) {
Expand Down
151 changes: 151 additions & 0 deletions test/telemetry.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
'use strict';

require('../testUtils');
var http = require('http');

var expect = require('chai').expect;
var testServer = null;

function createTestServer(handlerFunc, cb) {
var host = '127.0.0.1';
testServer = http.createServer(function(req, res) {
try {
handlerFunc(req, res);
} catch (e) {
res.writeHead(400, {'Content-Type': 'application/json'});
res.end(JSON.stringify({
error: {type: 'invalid_request_error', message: e.message}
}));
}
});
testServer.listen(0, host, function() {
var port = testServer.address().port;
cb(host, port);
});
}

describe('Client Telemetry', function() {
afterEach(function() {
if (testServer) {
testServer.close();
testServer = null;
}
});

it('Does not send telemetry when disabled', function(done) {
var numRequests = 0;

createTestServer(function (req, res) {
numRequests += 1;

var telemetry = req.headers['x-stripe-client-telemetry'];

switch (numRequests) {
case 1:
case 2:
expect(telemetry).to.not.exist;
break;
default:
expect.fail(`Should not have reached request ${numRequests}`);
}

res.setHeader('Request-Id', `req_${numRequests}`);
res.writeHead(200, {'Content-Type': 'application/json'});
res.end('{}');
}, function(host, port) {
const stripe = require('../lib/stripe')('sk_test_FEiILxKZwnmmocJDUjUNO6pa')
stripe.setHost(host, port, 'http');

stripe.balance.retrieve().then(function (res) {
return stripe.balance.retrieve();
}).then(function (res) {
expect(numRequests).to.equal(2);
done();
}).catch(done);
});
});

it('Sends client telemetry on the second request when enabled', function(done) {
var numRequests = 0;

createTestServer(function (req, res) {
numRequests += 1;

var telemetry = req.headers['x-stripe-client-telemetry'];

switch (numRequests) {
case 1:
expect(telemetry).to.not.exist;
break;
case 2:
expect(telemetry).to.exist;
expect(JSON.parse(telemetry).last_request_metrics.request_id)
.to.equal('req_1');
break;
default:
expect.fail(`Should not have reached request ${numRequests}`);
}

res.setHeader('Request-Id', `req_${numRequests}`);
res.writeHead(200, {'Content-Type': 'application/json'});
res.end('{}');
}, function(host, port) {
const stripe = require('../lib/stripe')('sk_test_FEiILxKZwnmmocJDUjUNO6pa')
stripe.setTelemetryEnabled(true);
stripe.setHost(host, port, 'http');

stripe.balance.retrieve().then(function (res) {
return stripe.balance.retrieve();
}).then(function (res) {
expect(numRequests).to.equal(2);
done();
}).catch(done);
});
});

it('Buffers metrics on concurrent requests', function(done) {
var numRequests = 0;

createTestServer(function (req, res) {
numRequests += 1;

var telemetry = req.headers['x-stripe-client-telemetry'];

switch (numRequests) {
case 1:
case 2:
expect(telemetry).to.not.exist;
break;
case 3:
case 4:
expect(telemetry).to.exist;
expect(JSON.parse(telemetry).last_request_metrics.request_id)
.to.be.oneOf(['req_1', 'req_2']);
break;
default:
expect.fail(`Should not have reached request ${numRequests}`);
}

res.setHeader('Request-Id', `req_${numRequests}`);
res.writeHead(200, {'Content-Type': 'application/json'});
res.end('{}');
}, function(host, port) {
const stripe = require('../lib/stripe')('sk_test_FEiILxKZwnmmocJDUjUNO6pa')
stripe.setTelemetryEnabled(true);
stripe.setHost(host, port, 'http');

Promise.all([
stripe.balance.retrieve(),
stripe.balance.retrieve()
]).then(function() {
return Promise.all([
stripe.balance.retrieve(),
stripe.balance.retrieve()
]);
}).then(function() {
expect(numRequests).to.equal(4);
done();
}).catch(done);
});
});
});

0 comments on commit bb2acf9

Please sign in to comment.