Skip to content

Commit bb2acf9

Browse files
jameshageman-stripebrandur-stripe
authored andcommitted
Add Stripe client telemetry to request headers (#557)
1 parent 00fcbcb commit bb2acf9

File tree

4 files changed

+196
-2
lines changed

4 files changed

+196
-2
lines changed

lib/StripeResource.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ StripeResource.extend = utils.protoExtend;
1616
StripeResource.method = require('./StripeMethod');
1717
StripeResource.BASIC_METHODS = require('./StripeMethod.basic');
1818

19+
StripeResource.MAX_BUFFERED_REQUEST_METRICS = 100;
20+
1921
/**
2022
* Encapsulates request logic for a Stripe Resource
2123
*/
@@ -125,6 +127,8 @@ StripeResource.prototype = {
125127
// lastResponse.
126128
res.requestId = headers['request-id'];
127129

130+
var requestDurationMs = Date.now() - req._requestStart;
131+
128132
var responseEvent = utils.removeEmpty({
129133
api_version: headers['stripe-version'],
130134
account: headers['stripe-account'],
@@ -133,7 +137,7 @@ StripeResource.prototype = {
133137
path: req._requestEvent.path,
134138
status: res.statusCode,
135139
request_id: res.requestId,
136-
elapsed: Date.now() - req._requestStart,
140+
elapsed: requestDurationMs,
137141
});
138142

139143
self._stripe._emitter.emit('response', responseEvent);
@@ -171,6 +175,9 @@ StripeResource.prototype = {
171175
null
172176
);
173177
}
178+
179+
self._recordRequestMetrics(res.requestId, requestDurationMs);
180+
174181
// Expose res object
175182
Object.defineProperty(response, 'lastResponse', {
176183
enumerable: false,
@@ -225,6 +232,28 @@ StripeResource.prototype = {
225232
return headers;
226233
},
227234

235+
_addTelemetryHeader: function(headers) {
236+
if (this._stripe.getTelemetryEnabled() && this._stripe._prevRequestMetrics.length > 0) {
237+
var metrics = this._stripe._prevRequestMetrics.shift();
238+
headers['X-Stripe-Client-Telemetry'] = JSON.stringify({
239+
'last_request_metrics': metrics
240+
});
241+
}
242+
},
243+
244+
_recordRequestMetrics: function(requestId, requestDurationMs) {
245+
if (this._stripe.getTelemetryEnabled() && requestId) {
246+
if (this._stripe._prevRequestMetrics.length > StripeResource.MAX_BUFFERED_REQUEST_METRICS) {
247+
utils.emitWarning('Request metrics buffer is full, dropping telemetry message.');
248+
} else {
249+
this._stripe._prevRequestMetrics.push({
250+
'request_id': requestId,
251+
'request_duration_ms': requestDurationMs,
252+
});
253+
}
254+
}
255+
},
256+
228257
_request: function(method, host, path, data, auth, options, callback) {
229258
var self = this;
230259
var requestData;
@@ -248,6 +277,8 @@ StripeResource.prototype = {
248277
Object.assign(headers, options.headers);
249278
}
250279

280+
self._addTelemetryHeader(headers);
281+
251282
makeRequest(apiVersion, headers);
252283
});
253284
}

lib/stripe.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ function Stripe(key, version) {
150150

151151
this.errors = require('./Error');
152152
this.webhooks = require('./Webhooks');
153+
154+
this._prevRequestMetrics = [];
155+
this.setTelemetryEnabled(false);
153156
}
154157

155158
Stripe.errors = require('./Error');
@@ -299,12 +302,19 @@ Stripe.prototype = {
299302
return formatted;
300303
},
301304

305+
setTelemetryEnabled: function(enableTelemetry) {
306+
this._enableTelemetry = enableTelemetry;
307+
},
308+
309+
getTelemetryEnabled: function() {
310+
return this._enableTelemetry;
311+
},
312+
302313
_prepResources: function() {
303314
for (var name in resources) {
304315
this[utils.pascalToCamelCase(name)] = new resources[name](this);
305316
}
306317
},
307-
308318
};
309319

310320
module.exports = Stripe;

lib/utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ var utils = module.exports = {
252252
return name[0].toLowerCase() + name.substring(1);
253253
}
254254
},
255+
256+
emitWarning: emitWarning,
255257
};
256258

257259
function emitWarning(warning) {

test/telemetry.spec.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'use strict';
2+
3+
require('../testUtils');
4+
var http = require('http');
5+
6+
var expect = require('chai').expect;
7+
var testServer = null;
8+
9+
function createTestServer(handlerFunc, cb) {
10+
var host = '127.0.0.1';
11+
testServer = http.createServer(function(req, res) {
12+
try {
13+
handlerFunc(req, res);
14+
} catch (e) {
15+
res.writeHead(400, {'Content-Type': 'application/json'});
16+
res.end(JSON.stringify({
17+
error: {type: 'invalid_request_error', message: e.message}
18+
}));
19+
}
20+
});
21+
testServer.listen(0, host, function() {
22+
var port = testServer.address().port;
23+
cb(host, port);
24+
});
25+
}
26+
27+
describe('Client Telemetry', function() {
28+
afterEach(function() {
29+
if (testServer) {
30+
testServer.close();
31+
testServer = null;
32+
}
33+
});
34+
35+
it('Does not send telemetry when disabled', function(done) {
36+
var numRequests = 0;
37+
38+
createTestServer(function (req, res) {
39+
numRequests += 1;
40+
41+
var telemetry = req.headers['x-stripe-client-telemetry'];
42+
43+
switch (numRequests) {
44+
case 1:
45+
case 2:
46+
expect(telemetry).to.not.exist;
47+
break;
48+
default:
49+
expect.fail(`Should not have reached request ${numRequests}`);
50+
}
51+
52+
res.setHeader('Request-Id', `req_${numRequests}`);
53+
res.writeHead(200, {'Content-Type': 'application/json'});
54+
res.end('{}');
55+
}, function(host, port) {
56+
const stripe = require('../lib/stripe')('sk_test_FEiILxKZwnmmocJDUjUNO6pa')
57+
stripe.setHost(host, port, 'http');
58+
59+
stripe.balance.retrieve().then(function (res) {
60+
return stripe.balance.retrieve();
61+
}).then(function (res) {
62+
expect(numRequests).to.equal(2);
63+
done();
64+
}).catch(done);
65+
});
66+
});
67+
68+
it('Sends client telemetry on the second request when enabled', function(done) {
69+
var numRequests = 0;
70+
71+
createTestServer(function (req, res) {
72+
numRequests += 1;
73+
74+
var telemetry = req.headers['x-stripe-client-telemetry'];
75+
76+
switch (numRequests) {
77+
case 1:
78+
expect(telemetry).to.not.exist;
79+
break;
80+
case 2:
81+
expect(telemetry).to.exist;
82+
expect(JSON.parse(telemetry).last_request_metrics.request_id)
83+
.to.equal('req_1');
84+
break;
85+
default:
86+
expect.fail(`Should not have reached request ${numRequests}`);
87+
}
88+
89+
res.setHeader('Request-Id', `req_${numRequests}`);
90+
res.writeHead(200, {'Content-Type': 'application/json'});
91+
res.end('{}');
92+
}, function(host, port) {
93+
const stripe = require('../lib/stripe')('sk_test_FEiILxKZwnmmocJDUjUNO6pa')
94+
stripe.setTelemetryEnabled(true);
95+
stripe.setHost(host, port, 'http');
96+
97+
stripe.balance.retrieve().then(function (res) {
98+
return stripe.balance.retrieve();
99+
}).then(function (res) {
100+
expect(numRequests).to.equal(2);
101+
done();
102+
}).catch(done);
103+
});
104+
});
105+
106+
it('Buffers metrics on concurrent requests', function(done) {
107+
var numRequests = 0;
108+
109+
createTestServer(function (req, res) {
110+
numRequests += 1;
111+
112+
var telemetry = req.headers['x-stripe-client-telemetry'];
113+
114+
switch (numRequests) {
115+
case 1:
116+
case 2:
117+
expect(telemetry).to.not.exist;
118+
break;
119+
case 3:
120+
case 4:
121+
expect(telemetry).to.exist;
122+
expect(JSON.parse(telemetry).last_request_metrics.request_id)
123+
.to.be.oneOf(['req_1', 'req_2']);
124+
break;
125+
default:
126+
expect.fail(`Should not have reached request ${numRequests}`);
127+
}
128+
129+
res.setHeader('Request-Id', `req_${numRequests}`);
130+
res.writeHead(200, {'Content-Type': 'application/json'});
131+
res.end('{}');
132+
}, function(host, port) {
133+
const stripe = require('../lib/stripe')('sk_test_FEiILxKZwnmmocJDUjUNO6pa')
134+
stripe.setTelemetryEnabled(true);
135+
stripe.setHost(host, port, 'http');
136+
137+
Promise.all([
138+
stripe.balance.retrieve(),
139+
stripe.balance.retrieve()
140+
]).then(function() {
141+
return Promise.all([
142+
stripe.balance.retrieve(),
143+
stripe.balance.retrieve()
144+
]);
145+
}).then(function() {
146+
expect(numRequests).to.equal(4);
147+
done();
148+
}).catch(done);
149+
});
150+
});
151+
});

0 commit comments

Comments
 (0)