Skip to content

Commit f8d6c3e

Browse files
authored
Merge pull request #4 from Moesif/add-capture-outgoing-support
Add: Support for capturing outgoing requests
2 parents 40a0992 + 53dfe09 commit f8d6c3e

File tree

9 files changed

+820
-21
lines changed

9 files changed

+820
-21
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ options.maskContent = function(moesifEvent) {
263263
Type: `Boolean`
264264
Set to true to print debug logs if you're having integration issues.
265265

266+
#### __`promisedBased`__
267+
Type: `Boolean`
268+
Set to true while using aws lambda async functions. For more details, please refer to - https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html.
269+
266270
For more documentation regarding what fields and meaning,
267271
see below or the [Moesif Node API Documentation](https://www.moesif.com/docs/api?javascript).
268272

@@ -291,6 +295,32 @@ user_id | _Recommend_ | Identifies this API call to a permanent user_id
291295
metadata | false | A JSON Object consisting of any custom metadata to be stored with this event.
292296

293297

298+
## Capture Outgoing
299+
300+
If you want to capture all outgoing API calls from your Node.js app to third parties like
301+
Stripe or to your own dependencies, call `startCaptureOutgoing()` to start capturing.
302+
303+
```javascript
304+
var moesifMiddleware = moesifExpress(options);
305+
moesifMiddleware.startCaptureOutgoing();
306+
```
307+
308+
The same set of above options is also applied to outgoing API calls, with a few key differences:
309+
310+
For options functions that take `req` and `res` as input arguments, the request and response objects passed in
311+
are not Express or Node.js req or res objects when the request is outgoing, but Moesif does mock
312+
some of the fields for convenience.
313+
Only a subset of the Node.js req/res fields are available. Specifically:
314+
315+
- *_mo_mocked*: Set to `true` if it is a mocked request or response object (i.e. outgoing API Call)
316+
- *headers*: object, a mapping of header names to header values. Case sensitive
317+
- *url*: string. Full request URL.
318+
- *method*: string. Method/verb such as GET or POST.
319+
- *statusCode*: number. Response HTTP status code
320+
- *getHeader*: function. (string) => string. Reads out a header on the request. Name is case insensitive
321+
- *get*: function. (string) => string. Reads out a header on the request. Name is case insensitive
322+
- *body*: JSON object. The request body as sent to Moesif
323+
294324

295325
## Update a Single User
296326

app.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55

66
const moesif = require('./lib');
7+
var http = require('http');
8+
var https = require('https');
79
console.log('Loading function');
810

911
const moesifOptions = {
@@ -15,7 +17,29 @@ const moesifOptions = {
1517
}
1618
};
1719

20+
var moesifMiddleware = moesif(moesifOptions);
21+
moesifMiddleware.startCaptureOutgoing();
22+
1823
exports.handler = function (event, context, callback) {
24+
// Outgoing API call to third party
25+
https.get(
26+
{
27+
host: 'jsonplaceholder.typicode.com',
28+
path: '/posts/1'
29+
},
30+
function(res) {
31+
var body = '';
32+
res.on('data', function(d) {
33+
body += d;
34+
});
35+
36+
res.on('end', function() {
37+
var parsed = JSON.parse(body);
38+
console.log(parsed);
39+
});
40+
}
41+
);
42+
1943
callback(null, {
2044
statusCode: '200',
2145
body: JSON.stringify({key: 'hello world'}),
@@ -25,4 +49,17 @@ exports.handler = function (event, context, callback) {
2549
});
2650
};
2751

52+
// Async Functions
53+
// Please set promisedBased configuration flag to true while using async functions. For more details, please refer to - https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html.
54+
55+
// moesifOptions.promisedBased = true;
56+
57+
// exports.handler = async (event, context) => {
58+
// const response = {
59+
// statusCode: 200,
60+
// body: JSON.stringify({ message: 'hello world' })
61+
// }
62+
// return response
63+
// }
64+
2865
exports.handler = moesif(moesifOptions, exports.handler);

lib/batcher.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
3+
function createBatcher(handleBatch, maxSize, maxTime) {
4+
return {
5+
dataArray: [],
6+
// using closure, so no need to keep as part of the object.
7+
// maxSize: maxSize,
8+
// maxTime: maxTime,
9+
add: function(data) {
10+
this.dataArray.push(data);
11+
if (this.dataArray.length >= maxSize) {
12+
this.flush();
13+
} else if (maxTime && this.dataArray.length === 1) {
14+
var self = this;
15+
this._timeout = setTimeout(function() {
16+
self.flush();
17+
}, maxTime);
18+
}
19+
},
20+
flush: function() {
21+
// note, in case the handleBatch is a
22+
// delayed function, then it swaps before
23+
// sending the current data.
24+
clearTimeout(this._timeout);
25+
this._lastFlush = Date.now();
26+
var currentDataArray = this.dataArray;
27+
this.dataArray = [];
28+
handleBatch(currentDataArray);
29+
}
30+
};
31+
}
32+
33+
module.exports = createBatcher;

lib/dataUtils.js

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
'use strict';
2+
3+
var url = require('url');
4+
var hash = require('crypto-js/md5');
5+
var isCreditCard = require('card-validator');
6+
var assign = require('lodash/assign');
7+
8+
function logMessage(debug, functionName, message) {
9+
if (debug) {
10+
console.log('MOESIF: [' + functionName + '] ' + message);
11+
}
12+
};
13+
14+
function _hashSensitive(jsonBody, debug) {
15+
if (jsonBody === null) return jsonBody;
16+
17+
if (Array.isArray(jsonBody)) {
18+
return jsonBody.map(function (item) {
19+
var itemType = typeof item;
20+
21+
if (itemType === 'number' || itemType === 'string') {
22+
var creditCardCheck = isCreditCard.number('' + item);
23+
if (creditCardCheck.isValid) {
24+
logMessage(debug, 'hashSensitive', 'looks like a credit card, performing hash.');
25+
return hash(item).toString();
26+
}
27+
}
28+
29+
return _hashSensitive(item, debug);
30+
});
31+
}
32+
33+
if (typeof jsonBody === 'object') {
34+
var returnObject = {};
35+
36+
Object.keys(jsonBody).forEach(function (key) {
37+
var innerVal = jsonBody[key];
38+
var innerValType = typeof innerVal;
39+
40+
if (key.toLowerCase().indexOf('password') !== -1 && typeof innerVal === 'string') {
41+
logMessage(debug, 'hashSensitive', 'key is password, so hashing the value.');
42+
returnObject[key] = hash(jsonBody[key]).toString();
43+
} else if (innerValType === 'number' || innerValType === 'string') {
44+
var creditCardCheck = isCreditCard.number('' + innerVal);
45+
if (creditCardCheck.isValid) {
46+
logMessage(debug, 'hashSensitive', 'a field looks like credit card, performing hash.');
47+
returnObject[key] = hash(jsonBody[key]).toString();
48+
} else {
49+
returnObject[key] = _hashSensitive(innerVal, debug);
50+
}
51+
} else {
52+
// recursive test for every value.
53+
returnObject[key] = _hashSensitive(innerVal, debug);
54+
}
55+
});
56+
57+
return returnObject;
58+
}
59+
60+
return jsonBody;
61+
}
62+
63+
function _getUrlFromRequestOptions(options, request) {
64+
if (typeof options === 'string') {
65+
options = url.parse(options);
66+
} else {
67+
// Avoid modifying the original options object.
68+
let originalOptions = options;
69+
options = {};
70+
if (originalOptions) {
71+
Object.keys(originalOptions).forEach((key) => {
72+
options[key] = originalOptions[key];
73+
});
74+
}
75+
}
76+
77+
// Oddly, url.format ignores path and only uses pathname and search,
78+
// so create them from the path, if path was specified
79+
if (options.path) {
80+
var parsedQuery = url.parse(options.path);
81+
options.pathname = parsedQuery.pathname;
82+
options.search = parsedQuery.search;
83+
}
84+
85+
// Simiarly, url.format ignores hostname and port if host is specified,
86+
// even if host doesn't have the port, but http.request does not work
87+
// this way. It will use the port if one is not specified in host,
88+
// effectively treating host as hostname, but will use the port specified
89+
// in host if it exists.
90+
if (options.host && options.port) {
91+
// Force a protocol so it will parse the host as the host, not path.
92+
// It is discarded and not used, so it doesn't matter if it doesn't match
93+
var parsedHost = url.parse('http://' + options.host);
94+
if (!parsedHost.port && options.port) {
95+
options.hostname = options.host;
96+
delete options.host;
97+
}
98+
}
99+
100+
// Mix in default values used by http.request and others
101+
options.protocol = options.protocol || (request.agent && request.agent.protocol) || undefined;
102+
options.hostname = options.hostname || 'localhost';
103+
104+
return url.format(options);
105+
}
106+
107+
function _bodyToBase64(body) {
108+
if (!body) {
109+
return body;
110+
}
111+
if (Buffer.isBuffer(body)) {
112+
return body.toString('base64');
113+
} else if (typeof body === 'string') {
114+
return Buffer.from(body).toString('base64');
115+
} else if (typeof body.toString === 'function') {
116+
return Buffer.from(body.toString()).toString('base64');
117+
} else {
118+
return '';
119+
}
120+
}
121+
122+
123+
function _safeJsonParse(body) {
124+
try {
125+
if (!Buffer.isBuffer(body) &&
126+
(typeof body === 'object' || Array.isArray(body))) {
127+
return {
128+
body: body,
129+
transferEncoding: undefined
130+
}
131+
}
132+
return {
133+
body: JSON.parse(body.toString()),
134+
transferEncoding: undefined
135+
}
136+
} catch (e) {
137+
return {
138+
body: _bodyToBase64(body),
139+
transferEncoding: 'base64'
140+
}
141+
}
142+
}
143+
144+
145+
function _startWithJson(body) {
146+
147+
var str;
148+
if (body && Buffer.isBuffer(body)) {
149+
str = body.slice(0, 1).toString('ascii');
150+
} else {
151+
str = body;
152+
}
153+
154+
if (str && typeof str === 'string') {
155+
var newStr = str.trim();
156+
if (newStr.startsWith('{') || newStr.startsWith('[')) {
157+
return true;
158+
}
159+
}
160+
return true;
161+
}
162+
163+
function _getEventModelFromRequestandResponse(
164+
requestOptions,
165+
request,
166+
requestTime,
167+
requestBody,
168+
response,
169+
responseTime,
170+
responseBody,
171+
) {
172+
var logData = {};
173+
logData.request = {};
174+
175+
logData.request.verb = typeof requestOptions === 'string' ? 'GET' : requestOptions.method || 'GET';
176+
logData.request.uri = _getUrlFromRequestOptions(requestOptions, request);
177+
logData.request.headers = requestOptions.headers || {};
178+
logData.request.time = requestTime;
179+
180+
if (requestBody) {
181+
var isReqBodyMaybeJson = _startWithJson(requestBody);
182+
183+
if (isReqBodyMaybeJson) {
184+
var parsedReqBody = _safeJsonParse(requestBody);
185+
186+
logData.request.transferEncoding = parsedReqBody.transferEncoding;
187+
logData.request.body = parsedReqBody.body;
188+
} else {
189+
logData.request.transferEncoding = 'base64';
190+
logData.request.body = _bodyToBase64(requestBody);
191+
}
192+
}
193+
194+
logData.response = {};
195+
logData.response.time = responseTime;
196+
logData.response.status = (response && response.statusCode) || 599;
197+
logData.response.headers = assign({}, (response && response.headers) || {});
198+
199+
if (responseBody) {
200+
var isResBodyMaybeJson = _startWithJson(responseBody);
201+
202+
if (isResBodyMaybeJson) {
203+
var parsedResBody = _safeJsonParse(responseBody);
204+
205+
logData.response.transferEncoding = parsedResBody.transferEncoding;
206+
logData.response.body = parsedResBody.body;
207+
} else {
208+
logData.response.transferEncoding = 'base64';
209+
logData.response.body = _bodyToBase64(responseBody);
210+
}
211+
}
212+
213+
return logData;
214+
}
215+
216+
module.exports = {
217+
getUrlFromRequestOptions: _getUrlFromRequestOptions,
218+
getEventModelFromRequestandResponse: _getEventModelFromRequestandResponse,
219+
safeJsonParse: _safeJsonParse,
220+
startWithJson: _startWithJson,
221+
bodyToBase64: _bodyToBase64,
222+
hashSensitive: _hashSensitive
223+
};

0 commit comments

Comments
 (0)