@glimpse/glimpse-agent-node
Advanced tools
Comparing version 0.20.8 to 0.20.9
@@ -692,9 +692,16 @@ 'use strict'; | ||
function wrapOnRejected(onRejected) { | ||
function wrapOnRejected(...args) { | ||
const asyncId = self.getNextAsyncId(); | ||
const asyncState = self.raiseAsyncTransition(self.currentAsyncId, asyncId); | ||
if (typeof onRejected === 'function') { | ||
self.wrapCallbackInParameter(0, arguments, {}, asyncId, asyncState, catchNamePath); | ||
// bluebird API allows for "filtered catches", where the first n -1 arguments are constructors | ||
// or filter functions, and the last argument is the catch handler. Account for that here. | ||
let callbackIndex = 0; | ||
if (args.length > 1 && typeof args[args.length - 1] === 'function') { | ||
callbackIndex = args.length - 1; | ||
} | ||
if (typeof args[callbackIndex] === 'function') { | ||
self.wrapCallbackInParameter(callbackIndex, arguments, {}, asyncId, asyncState, catchNamePath); | ||
} | ||
const ret = originalCatch.apply(this, arguments); | ||
@@ -701,0 +708,0 @@ |
@@ -5,3 +5,2 @@ 'use strict'; | ||
import { IErrorReportingService, createHttpServerError, createHttpServerEarlyRequestTerminationError } from '@glimpse/glimpse-common'; | ||
import { IContext } from '../messaging/IContextManager'; | ||
import { IAgent } from '../IAgent'; | ||
@@ -62,255 +61,304 @@ import { IServerRequestInspector, IPartSummary } from './IServerRequestInspector'; | ||
private setupServerProxy(agent: IAgent, httpModule): void { | ||
private patchCreateServer(agent: IAgent, httpModule): void { | ||
const oldCreateServer = httpModule.createServer; | ||
const self = this; | ||
const maxBodySize = HttpHelper.getMaxBodySize(agent.providers.configSettings); | ||
httpModule.createServer = function createServer(options, cb, ...args) { | ||
function internalCallback(req: http.IncomingMessage, res: http.ServerResponse, ...rest) { | ||
const requestStartTime: DateTimeValue = self.getCurrentTimeStamp(); | ||
agent.providers.contextManager.runInNewContext(req, (context: IContext) => { | ||
// store context on req/response | ||
HttpHelper.setContext(req, context); | ||
HttpHelper.setContext(res, context); | ||
httpModule.createServer = function glimpseHttpCreateServer(...args) { | ||
let server; | ||
self.raiseRequestStartEvent(req, res, requestStartTime); | ||
// NOTE: https.createServer() and http.createServer() have different signatures: | ||
// - http.createServer([callback]) | ||
// - https.createServer(options[, callback]) | ||
// | ||
// We can't inspect the callback type because the callback is optional, | ||
// but we can inspect the `options` parameter since it is required for | ||
// HTTPS calls and HTTP calls don't accept an options object | ||
// | ||
// Note that we do *not* pass the callback into the old createServer(); | ||
// it's attached to the `request` event after `on` has been patched. | ||
// It is possible in some circumstances that `res.end()` is | ||
// called before the `data` event on the `req` object is | ||
// fired. In this case, we check this flag and send the before | ||
// event immediately before sending the end event. | ||
let requestEndSent = false; | ||
let cb; | ||
// Note: the User Inspector class was rolled into this one | ||
// because the begin/end events weren't fine graind enough | ||
// to set these headers at the appropriate time. Once this | ||
// module is ported to the new proxy paradigm, this can be | ||
// split back into a separate inspector | ||
// BEGIN code from UserInspector | ||
const requestCookies = RequestHelper.parseCookies(req); | ||
const userId = requestCookies ? requestCookies[SESSION_COOKIE] : undefined; | ||
if (!userId) { | ||
ResponseHelper.setCookie(res, SESSION_COOKIE, GuidHelper.newGuid(false)); | ||
} | ||
// END code from UserInspector | ||
const isHttps = args.length && typeof args[0] === 'object'; | ||
res.setHeader('X-Glimpse-ContextId', context.id); | ||
if (isHttps && args.length >= 1 && typeof args[1] === 'function') { | ||
cb = args[1]; | ||
args.splice(1, 1); | ||
} | ||
else if (!isHttps && args.length && typeof args[0] === 'function') { | ||
cb = args[0]; | ||
args.splice(0, 1); | ||
} | ||
// General performance note for this implementation: this has been identified | ||
// as a hot path for performance, so there are places where maintainability | ||
// and readability are sacrificed for performance. Specifically, there is | ||
// repeated code in here that could be abstracted into helper methods, but | ||
// would incure the extra stack frame and slow things down | ||
server = oldCreateServer.apply(this, args); | ||
// Note on Buffers. We use Buffers to store body requests, and we | ||
// create new buffers a few times as well. We use the Buffer consructor | ||
// to do this for backwards compatibility reasons, but we should | ||
// migrate away some day. There is a security risk with using the | ||
// Buffer constructor, which is why it's been deprecated. More info: | ||
// https://nodejs.org/api/buffer.html#buffer_buffer_from_buffer_alloc_and_buffer_allocunsafe | ||
// NOTE: Any number of `request` listeners may be attached to the server, | ||
// either attached indirectly via createServer(cb) or attached directly | ||
// via server.on('request', cb). | ||
// | ||
// We must ensure that all of them execute within the same Glimpse context. | ||
// We do this by patching server.on() and wrapping `request` listeners, | ||
// then creating/adding the context. | ||
const oldOn = server.on; | ||
server.on = function glimpseServerOn(eventName: string, listener, ...onArgs) { | ||
if (eventName === 'request') { | ||
const oldListener = listener; | ||
// Chunks may be read back as either Buffers or strings. For now we store | ||
// them as an array of chunks, and let inspectors figure out the best way | ||
// to normalize them. | ||
let requestBodyChunks = []; | ||
let requestBodyLength = 0; | ||
listener = (req, res) => { | ||
// NOTE: Glimpse initialization in the user's app is synchronous (e.g. `glimpse.init()`). | ||
// However, there are some Glimpse services that require asynchronous initialization. | ||
// In those cases, we defer initialization until just prior to handling the first | ||
// request (as that's the first asynchronous hook point provided by Node). When | ||
// initialization is complete, we continue processing the request. | ||
agent.providers.deferredInitializationManager.init(err => { | ||
if (err) { | ||
throw err; | ||
} | ||
let bufferedData = []; | ||
let isBufferingData = true; | ||
let context = HttpHelper.getContext(req); | ||
let isMultiPartFormData: boolean = undefined; | ||
let multiPartFormSummarizer: IMultiPartFormSummarizer = undefined; | ||
// If no context currently exists, create one and attach to request/response objects... | ||
if (!context) { | ||
context = agent.providers.contextManager.createContext(req); | ||
req.on('data', (chunk) => { | ||
agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'data\')', context.id); | ||
// set up a parser to parse out headers if this is a multi-part form request | ||
if (isMultiPartFormData === undefined) { | ||
multiPartFormSummarizer = createMultiPartFormSummarizer(req.headers['content-type']); | ||
if (multiPartFormSummarizer) { | ||
isMultiPartFormData = true; | ||
HttpHelper.setContext(req, context); | ||
HttpHelper.setContext(res, context); | ||
} | ||
else { | ||
isMultiPartFormData = false; | ||
} | ||
} | ||
if (isBufferingData) { | ||
bufferedData.push(chunk); | ||
} | ||
// Run the handler within the context... | ||
return agent.providers.contextManager.runInContext( | ||
context, | ||
() => oldListener(req, res)); | ||
}); | ||
}; | ||
} | ||
if (isMultiPartFormData) { | ||
multiPartFormSummarizer.addChunk(chunk); | ||
} | ||
return oldOn.call(this, eventName, listener, ...onArgs); | ||
}; | ||
const originalChunkLength = chunk.length; | ||
if (requestBodyLength < maxBodySize) { | ||
if (requestBodyLength + originalChunkLength >= maxBodySize) { | ||
chunk = chunk.slice(0, maxBodySize - requestBodyLength); | ||
} | ||
requestBodyChunks.push(chunk); | ||
} | ||
requestBodyLength += originalChunkLength; | ||
}); | ||
// Attach HttpProxy-specific logic to `request` event (which wraps any callback passed to createServer())... | ||
server.on('request', self.setupServerProxy(agent, cb)); | ||
let isBufferingEnd = false; | ||
return server; | ||
}; | ||
} | ||
req.on('end', () => { | ||
isBufferingEnd = true; | ||
private setupServerProxy(agent: IAgent, cb) { | ||
const self = this; | ||
const maxBodySize = HttpHelper.getMaxBodySize(agent.providers.configSettings); | ||
return function internalCallback(req: http.IncomingMessage, res: http.ServerResponse, ...rest) { | ||
const context = HttpHelper.getContext(req); | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'end\')', context.id); | ||
if (!requestEndSent) { | ||
self.raiseRequestEndEvent(req, res, requestBodyChunks, requestBodyLength, requestStartTime, multiPartFormSummarizer ? multiPartFormSummarizer.getParts() : []); | ||
requestEndSent = true; | ||
} | ||
}); | ||
const requestStartTime: DateTimeValue = self.getCurrentTimeStamp(); | ||
req.on('error', (err: Error | string) => { | ||
self.errorReportingService.reportError(createHttpServerError(err)); | ||
}); | ||
self.raiseRequestStartEvent(req, res, requestStartTime); | ||
res.on('error', (err: Error | string) => { | ||
self.errorReportingService.reportError(createHttpServerError(err)); | ||
}); | ||
// It is possible in some circumstances that `res.end()` is | ||
// called before the `data` event on the `req` object is | ||
// fired. In this case, we check this flag and send the before | ||
// event immediately before sending the end event. | ||
let requestEndSent = false; | ||
// NOTE: We MUST be subscribed to the 'data' and 'end' events PRIOR to patching 'on()'. | ||
// Note: the User Inspector class was rolled into this one | ||
// because the begin/end events weren't fine graind enough | ||
// to set these headers at the appropriate time. Once this | ||
// module is ported to the new proxy paradigm, this can be | ||
// split back into a separate inspector | ||
// BEGIN code from UserInspector | ||
const requestCookies = RequestHelper.parseCookies(req); | ||
const userId = requestCookies ? requestCookies[SESSION_COOKIE] : undefined; | ||
if (!userId) { | ||
ResponseHelper.setCookie(res, SESSION_COOKIE, GuidHelper.newGuid(false)); | ||
} | ||
// END code from UserInspector | ||
const oldOn = req.on; | ||
req.on = function newOn(event, onCallback, ...onRest) { | ||
if (isBufferingData && event === 'data') { | ||
try { | ||
bufferedData.forEach(chunk => { | ||
onCallback.call(this, chunk); | ||
}); | ||
} | ||
finally { | ||
bufferedData = []; | ||
isBufferingData = false; | ||
} | ||
} | ||
else if (isBufferingEnd && event === 'end') { | ||
onCallback.call(this); | ||
} | ||
res.setHeader('X-Glimpse-ContextId', context.id); | ||
return oldOn.call(this, event, onCallback, ...onRest); | ||
}; | ||
// General performance note for this implementation: this has been identified | ||
// as a hot path for performance, so there are places where maintainability | ||
// and readability are sacrificed for performance. Specifically, there is | ||
// repeated code in here that could be abstracted into helper methods, but | ||
// would incure the extra stack frame and slow things down | ||
// Note: it's possible to write data using the `end` method as well, | ||
// but that method calls `write` under the hood, and patching both | ||
// leads to a doubly patched write method, which duplicates the body | ||
let responseBodyChunks = []; | ||
let responseBodyLength = 0; | ||
const oldWrite = res.write; | ||
let responseStartTime = undefined; | ||
res.write = function (chunk, encoding?, ...writeArgs): boolean { | ||
if (!responseStartTime) { | ||
responseStartTime = self.getCurrentTimeStamp(); | ||
self.raiseResponseStartEvent(req, res, responseStartTime); | ||
} | ||
// Note on Buffers. We use Buffers to store body requests, and we | ||
// create new buffers a few times as well. We use the Buffer consructor | ||
// to do this for backwards compatibility reasons, but we should | ||
// migrate away some day. There is a security risk with using the | ||
// Buffer constructor, which is why it's been deprecated. More info: | ||
// https://nodejs.org/api/buffer.html#buffer_buffer_from_buffer_alloc_and_buffer_allocunsafe | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(response.write())', context.id); | ||
// Chunks may be read back as either Buffers or strings. For now we store | ||
// them as an array of chunks, and let inspectors figure out the best way | ||
// to normalize them. | ||
let requestBodyChunks = []; | ||
let requestBodyLength = 0; | ||
// Short circuit if we're not actually writing anything | ||
if (typeof chunk === 'function' || typeof chunk === 'undefined') { | ||
return oldWrite.call(this, chunk, encoding, ...writeArgs); | ||
} | ||
let bufferedData = []; | ||
let isBufferingData = true; | ||
// If we don't have the necessary information to normalize | ||
// to a string, the underlying API will throw, so we short | ||
// circuit here and call the underlying API | ||
if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) { | ||
return oldWrite.call(this, chunk, encoding, ...writeArgs); | ||
} | ||
let isMultiPartFormData: boolean = undefined; | ||
let multiPartFormSummarizer: IMultiPartFormSummarizer = undefined; | ||
// Save part or all of the chunk to the set of chunks, | ||
// truncating if necessary to keep the set under the | ||
// max body size | ||
const originalChunkLength = chunk.length; | ||
let normalizedChunk = chunk; | ||
if (responseBodyLength < maxBodySize) { | ||
if (responseBodyLength + originalChunkLength >= maxBodySize) { | ||
normalizedChunk = normalizedChunk.slice(0, maxBodySize - responseBodyLength); | ||
} | ||
responseBodyChunks.push(normalizedChunk); | ||
} | ||
responseBodyLength += originalChunkLength; | ||
req.on('data', (chunk) => { | ||
agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'data\')', context.id); | ||
return oldWrite.call(this, chunk, encoding, ...writeArgs); | ||
}; | ||
// set up a parser to parse out headers if this is a multi-part form request | ||
if (isMultiPartFormData === undefined) { | ||
multiPartFormSummarizer = createMultiPartFormSummarizer(req.headers['content-type']); | ||
if (multiPartFormSummarizer) { | ||
isMultiPartFormData = true; | ||
} | ||
else { | ||
isMultiPartFormData = false; | ||
} | ||
} | ||
// We override the setHeader method so we can intercept the | ||
// content-type and set the request ID header if this is the | ||
// first request for the page. We will know if this request | ||
// was the first request for the page if it's a) a request | ||
// for HTML and b) it's not an AJAX request because it's not | ||
// possible to request HTML content after the initial request | ||
// *except* via AJAX. | ||
const oldSetHeader = res.setHeader; | ||
res.setHeader = function setHeader(name, value, ...setHeaderArgs) { | ||
oldSetHeader.call(this, name, value, ...setHeaderArgs); | ||
if (name.toLowerCase() === 'content-type' && value.indexOf('text/html') === 0 && RequestHelper.header(req, IS_AJAX_HEADER) !== 'true') { | ||
ResponseHelper.setCookie(res, REQUEST_ID_COOKIE, context.id); | ||
} | ||
}; | ||
if (isBufferingData) { | ||
bufferedData.push(chunk); | ||
} | ||
res.on('finish', () => { | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'finish\')', context.id); | ||
if (isMultiPartFormData) { | ||
multiPartFormSummarizer.addChunk(chunk); | ||
} | ||
if (!requestEndSent) { | ||
// If we got here, it means that the `res.end()` method | ||
// was called before the request `end` event was fired. | ||
// This could happen for a variety of reasons, mostly | ||
// when the user calls `res.end` before the request has | ||
// finished flushing. Most often, this happens because | ||
// the body has not been recieved, but we try and send | ||
// whatever body we have receieved so far. | ||
self.raiseRequestEndEvent(req, res, requestBodyChunks, requestBodyLength, requestStartTime, multiPartFormSummarizer ? multiPartFormSummarizer.getParts() : []); | ||
requestEndSent = true; | ||
const originalChunkLength = chunk.length; | ||
if (requestBodyLength < maxBodySize) { | ||
if (requestBodyLength + originalChunkLength >= maxBodySize) { | ||
chunk = chunk.slice(0, maxBodySize - requestBodyLength); | ||
} | ||
requestBodyChunks.push(chunk); | ||
} | ||
requestBodyLength += originalChunkLength; | ||
}); | ||
// We check to see if content length was set, and if it | ||
// was and we haven't seen that much of the body yet, | ||
// we report an error that not all of the body was captured | ||
const contentLength = RequestHelper.header(req, 'content-length'); | ||
if (contentLength && contentLength > requestBodyLength && self.errorReportingService) { | ||
self.errorReportingService.reportError(createHttpServerEarlyRequestTerminationError(req.url)); | ||
} | ||
} | ||
responseStartTime = responseStartTime || self.getCurrentTimeStamp(); | ||
self.raiseResponseEndEvent(req, res, responseBodyChunks, responseBodyLength, responseStartTime); | ||
}); | ||
let isBufferingEnd = false; | ||
if (cb) { | ||
cb(req, res, ...rest); | ||
req.on('end', () => { | ||
isBufferingEnd = true; | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'end\')', context.id); | ||
if (!requestEndSent) { | ||
self.raiseRequestEndEvent(req, res, requestBodyChunks, requestBodyLength, requestStartTime, multiPartFormSummarizer ? multiPartFormSummarizer.getParts() : []); | ||
requestEndSent = true; | ||
} | ||
}); | ||
req.on('error', (err: Error | string) => { | ||
self.errorReportingService.reportError(createHttpServerError(err)); | ||
}); | ||
res.on('error', (err: Error | string) => { | ||
self.errorReportingService.reportError(createHttpServerError(err)); | ||
}); | ||
// NOTE: We MUST be subscribed to the 'data' and 'end' events PRIOR to patching 'on()'. | ||
const oldOn = req.on; | ||
req.on = function newOn(event, onCallback, ...onRest) { | ||
if (isBufferingData && event === 'data') { | ||
try { | ||
bufferedData.forEach(chunk => { | ||
onCallback.call(this, chunk); | ||
}); | ||
} | ||
}); | ||
finally { | ||
bufferedData = []; | ||
isBufferingData = false; | ||
} | ||
} | ||
else if (isBufferingEnd && event === 'end') { | ||
onCallback.call(this); | ||
} | ||
return oldOn.call(this, event, onCallback, ...onRest); | ||
}; | ||
// NOTE: Glimpse initialization in the user's app is synchronous (e.g. `glimpse.init()`). | ||
// However, there are some Glimpse services that require asynchronous initialization. | ||
// In those cases, we defer initialization until just prior to handling the first | ||
// request (as that's the first asynchronous hook point provided by Node). When | ||
// initialization is complete, we continue processing the request. | ||
function deferredCallback(req, res, ...rest) { | ||
agent.providers.deferredInitializationManager.init(err => { | ||
if (err) { | ||
throw err; | ||
// Note: it's possible to write data using the `end` method as well, | ||
// but that method calls `write` under the hood, and patching both | ||
// leads to a doubly patched write method, which duplicates the body | ||
let responseBodyChunks = []; | ||
let responseBodyLength = 0; | ||
const oldWrite = res.write; | ||
let responseStartTime = undefined; | ||
res.write = function (chunk, encoding?, ...writeArgs): boolean { | ||
if (!responseStartTime) { | ||
responseStartTime = self.getCurrentTimeStamp(); | ||
self.raiseResponseStartEvent(req, res, responseStartTime); | ||
} | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(response.write())', context.id); | ||
// Short circuit if we're not actually writing anything | ||
if (typeof chunk === 'function' || typeof chunk === 'undefined') { | ||
return oldWrite.call(this, chunk, encoding, ...writeArgs); | ||
} | ||
// If we don't have the necessary information to normalize | ||
// to a string, the underlying API will throw, so we short | ||
// circuit here and call the underlying API | ||
if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) { | ||
return oldWrite.call(this, chunk, encoding, ...writeArgs); | ||
} | ||
// Save part or all of the chunk to the set of chunks, | ||
// truncating if necessary to keep the set under the | ||
// max body size | ||
const originalChunkLength = chunk.length; | ||
let normalizedChunk = chunk; | ||
if (responseBodyLength < maxBodySize) { | ||
if (responseBodyLength + originalChunkLength >= maxBodySize) { | ||
normalizedChunk = normalizedChunk.slice(0, maxBodySize - responseBodyLength); | ||
} | ||
responseBodyChunks.push(normalizedChunk); | ||
} | ||
responseBodyLength += originalChunkLength; | ||
internalCallback(req, res, ...rest); | ||
}); | ||
} | ||
return oldWrite.call(this, chunk, encoding, ...writeArgs); | ||
}; | ||
// Note: https.createServer and http.createServer have different signatures: | ||
// http.createServer([callback]) | ||
// https.createServer(options[, callback]) | ||
// We can't inspect the callback type because the callback is optional, | ||
// but we can inspect the `options` parameter since it is required for | ||
// HTTPS calls and HTTP calls don't accept an options object | ||
if (typeof options !== 'object') { | ||
cb = options; | ||
return oldCreateServer.call(this, deferredCallback, ...args); | ||
} else { | ||
return oldCreateServer.call(this, options, deferredCallback, ...args); | ||
// We override the setHeader method so we can intercept the | ||
// content-type and set the request ID header if this is the | ||
// first request for the page. We will know if this request | ||
// was the first request for the page if it's a) a request | ||
// for HTML and b) it's not an AJAX request because it's not | ||
// possible to request HTML content after the initial request | ||
// *except* via AJAX. | ||
const oldSetHeader = res.setHeader; | ||
res.setHeader = function setHeader(name, value, ...setHeaderArgs) { | ||
oldSetHeader.call(this, name, value, ...setHeaderArgs); | ||
if (name.toLowerCase() === 'content-type' && value.indexOf('text/html') === 0 && RequestHelper.header(req, IS_AJAX_HEADER) !== 'true') { | ||
ResponseHelper.setCookie(res, REQUEST_ID_COOKIE, context.id); | ||
} | ||
}; | ||
res.on('finish', () => { | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'finish\')', context.id); | ||
if (!requestEndSent) { | ||
// If we got here, it means that the `res.end()` method | ||
// was called before the request `end` event was fired. | ||
// This could happen for a variety of reasons, mostly | ||
// when the user calls `res.end` before the request has | ||
// finished flushing. Most often, this happens because | ||
// the body has not been recieved, but we try and send | ||
// whatever body we have receieved so far. | ||
self.raiseRequestEndEvent(req, res, requestBodyChunks, requestBodyLength, requestStartTime, multiPartFormSummarizer ? multiPartFormSummarizer.getParts() : []); | ||
requestEndSent = true; | ||
// We check to see if content length was set, and if it | ||
// was and we haven't seen that much of the body yet, | ||
// we report an error that not all of the body was captured | ||
const contentLength = RequestHelper.header(req, 'content-length'); | ||
if (contentLength && contentLength > requestBodyLength && self.errorReportingService) { | ||
self.errorReportingService.reportError(createHttpServerEarlyRequestTerminationError(req.url)); | ||
} | ||
} | ||
responseStartTime = responseStartTime || self.getCurrentTimeStamp(); | ||
self.raiseResponseEndEvent(req, res, responseBodyChunks, responseBodyLength, responseStartTime); | ||
}); | ||
if (cb) { | ||
cb(req, res, ...rest); | ||
} | ||
@@ -342,3 +390,3 @@ }; | ||
this.proxiedModules.push(httpModule); | ||
this.setupServerProxy(agent, httpModule); | ||
this.patchCreateServer(agent, httpModule); | ||
this.setupInspectorExtensions(agent); | ||
@@ -345,0 +393,0 @@ |
@@ -36,2 +36,7 @@ | ||
/** | ||
* keep track of any strings we can't parse so we only report an error once per string | ||
*/ | ||
private reportedErrors: { [key: string]: boolean } = {}; | ||
private errorReportingService: IErrorReportingService; | ||
@@ -212,11 +217,17 @@ | ||
} | ||
else if (openParen && closeParen && source.substring(openParen + 1, closeParen) === 'native') { | ||
// case ' at Array.forEach (native)' | ||
functionName = source.substring(7, openParen).trim(); | ||
fileName = 'native'; | ||
line = '0'; | ||
column = '0'; | ||
else if (openParen && closeParen) { | ||
const tryFileName = source.substring(openParen + 1, closeParen); | ||
if (tryFileName === 'native' || tryFileName === '<anonymous>') { | ||
// case ' at Array.forEach (native)' or ' at callFn.next (<anonymous>)' | ||
functionName = source.substring(7, openParen).trim(); | ||
fileName = tryFileName; | ||
line = '0'; | ||
column = '0'; | ||
} | ||
} | ||
else { | ||
if (this.errorReportingService) { | ||
// if filename is undefined, we failed to parse the frame | ||
if (!fileName) { | ||
if (this.errorReportingService && !this.reportedErrors[source]) { | ||
this.reportedErrors[source] = true; | ||
this.errorReportingService.reportError(createStackHelperUnsupportedStackFrameFormat(source)); | ||
@@ -223,0 +234,0 @@ } |
'use strict'; | ||
import { IContextManager, IRunInContextCallback } from './IContextManager'; | ||
import { IContext, IContextManager, IRunInContextCallback } from './IContextManager'; | ||
import { ContextManagerBase } from './ContextManagerBase'; | ||
@@ -71,4 +71,3 @@ import { IAsyncTrack, IAsyncTrackEvents, getAsyncTrack } from './../async-track/async-track'; | ||
// tslint:disable-next-line:no-any | ||
public runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): any { | ||
const context = this.createContext(req); | ||
public runInContext(context: IContext, callback: IRunInContextCallback): any { | ||
const callbackWrapper = function callbackWrapper() { | ||
@@ -81,8 +80,9 @@ return callback(context); | ||
// tslint:disable-next-line:no-any | ||
public runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): any { | ||
return this.runInContext(this.createContext(req), callback); | ||
} | ||
// tslint:disable-next-line:no-any | ||
public runInNullContext(callback: IRunInContextCallback): any { | ||
const context = undefined; | ||
const callbackWrapper = function callbackWrapper() { | ||
return callback(context); | ||
}; | ||
return this.asyncTrack.runInContext(callbackWrapper, this.createAsyncState(undefined)); | ||
return this.runInContext(undefined, callback); | ||
} | ||
@@ -89,0 +89,0 @@ |
@@ -38,2 +38,6 @@ 'use strict'; | ||
public runInContext(context: IContext, callback: IRunInContextCallback) { | ||
throw new Error('please override'); | ||
} | ||
public runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback) { | ||
@@ -40,0 +44,0 @@ throw new Error('please override'); |
'use strict'; | ||
import { IContextManager, IRunInContextCallback } from './IContextManager'; | ||
import { IContext, IContextManager, IRunInContextCallback } from './IContextManager'; | ||
import { ContextManagerBase } from './ContextManagerBase'; | ||
@@ -24,5 +24,4 @@ | ||
// tslint:disable-next-line:no-any | ||
public runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): any { | ||
public runInContext(context: IContext, callback: IRunInContextCallback): any { | ||
const wrapper = () => { | ||
const context = this.createContext(req); | ||
this._namespace.set(ContextManagerContinuationLocalStorage.GLIMPSE_CONTEXT, context); | ||
@@ -36,10 +35,9 @@ return callback(context); | ||
// tslint:disable-next-line:no-any | ||
public runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): any { | ||
return this.runInContext(this.createContext(req), callback); | ||
}; | ||
// tslint:disable-next-line:no-any | ||
public runInNullContext(callback: IRunInContextCallback): any { | ||
const wrapper = () => { | ||
const context = undefined; | ||
this._namespace.set(ContextManagerContinuationLocalStorage.GLIMPSE_CONTEXT, context); | ||
return callback(context); | ||
}; | ||
const boundFunction = this._namespace.bind(wrapper, this._namespace.createContext()); | ||
return boundFunction(); | ||
return this.runInContext(undefined, callback); | ||
}; | ||
@@ -46,0 +44,0 @@ |
@@ -26,4 +26,6 @@ 'use strict'; | ||
export interface IContextManager { | ||
createContext(req: http.IncomingMessage): IContext; | ||
currentContext(): IContext; | ||
isWithinContext(): boolean; | ||
runInContext(context: IContext, callback: IRunInContextCallback); | ||
runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback); | ||
@@ -30,0 +32,0 @@ runInNullContext(callback: IRunInContextCallback); |
{ | ||
"name": "@glimpse/glimpse-agent-node", | ||
"version": "0.20.8", | ||
"version": "0.20.9", | ||
"license": "See license in license.md", | ||
@@ -34,3 +34,3 @@ "main": "./release/index.js", | ||
"dependencies": { | ||
"@glimpse/glimpse-common": "0.20.8", | ||
"@glimpse/glimpse-common": "0.20.9", | ||
"config-chain": "^1.1.10", | ||
@@ -74,2 +74,3 @@ "continuation-local-storage": "^3.1.4", | ||
"cookie-parser": "^1.4.1", | ||
"create-error": "^0.3.1", | ||
"del": "^2.2.0", | ||
@@ -76,0 +77,0 @@ "express": "^4.13.4", |
@@ -556,8 +556,18 @@ 'use strict'; | ||
var self = this; | ||
function wrapOnRejected(onRejected) { | ||
function wrapOnRejected() { | ||
var args = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
args[_i] = arguments[_i]; | ||
} | ||
var asyncId = self.getNextAsyncId(); | ||
var asyncState = self.raiseAsyncTransition(self.currentAsyncId, asyncId); | ||
if (typeof onRejected === 'function') { | ||
self.wrapCallbackInParameter(0, arguments, {}, asyncId, asyncState, catchNamePath); | ||
// bluebird API allows for "filtered catches", where the first n -1 arguments are constructors | ||
// or filter functions, and the last argument is the catch handler. Account for that here. | ||
var callbackIndex = 0; | ||
if (args.length > 1 && typeof args[args.length - 1] === 'function') { | ||
callbackIndex = args.length - 1; | ||
} | ||
if (typeof args[callbackIndex] === 'function') { | ||
self.wrapCallbackInParameter(callbackIndex, arguments, {}, asyncId, asyncState, catchNamePath); | ||
} | ||
var ret = originalCatch.apply(this, arguments); | ||
@@ -564,0 +574,0 @@ // other libraries may proxy promises, so we want to be sure that our proxies are always hooked up. |
@@ -1,1 +0,1 @@ | ||
export declare function runPromiseTests(promiseCtor: any): void; | ||
export declare function runPromiseTests(promiseCtor: any, description: any): void; |
@@ -17,3 +17,4 @@ import { IErrorReportingService } from '@glimpse/glimpse-common'; | ||
private getCurrentTimeStamp(); | ||
private setupServerProxy(agent, httpModule); | ||
private patchCreateServer(agent, httpModule); | ||
private setupServerProxy(agent, cb); | ||
private setupInspectorExtensions(agent); | ||
@@ -20,0 +21,0 @@ addServerInspector(inspector: IServerRequestInspector): void; |
@@ -21,2 +21,6 @@ /// <reference types="node" /> | ||
private mapCache; | ||
/** | ||
* keep track of any strings we can't parse so we only report an error once per string | ||
*/ | ||
private reportedErrors; | ||
private errorReportingService; | ||
@@ -23,0 +27,0 @@ constructor(errorService: IErrorReportingService); |
/// <reference types="node" /> | ||
import { IContextManager, IRunInContextCallback } from './IContextManager'; | ||
import { IContext, IContextManager, IRunInContextCallback } from './IContextManager'; | ||
import { ContextManagerBase } from './ContextManagerBase'; | ||
@@ -16,2 +16,3 @@ import { IAsyncTrack } from './../async-track/async-track'; | ||
}; | ||
runInContext(context: IContext, callback: IRunInContextCallback): any; | ||
runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): any; | ||
@@ -18,0 +19,0 @@ runInNullContext(callback: IRunInContextCallback): any; |
@@ -13,2 +13,3 @@ /// <reference types="node" /> | ||
currentContext(): IContext; | ||
runInContext(context: IContext, callback: IRunInContextCallback): void; | ||
runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): void; | ||
@@ -15,0 +16,0 @@ runInNullContext(callback: IRunInContextCallback): void; |
/// <reference types="node" /> | ||
import { IContextManager, IRunInContextCallback } from './IContextManager'; | ||
import { IContext, IContextManager, IRunInContextCallback } from './IContextManager'; | ||
import { ContextManagerBase } from './ContextManagerBase'; | ||
@@ -10,2 +10,3 @@ import * as http from 'http'; | ||
init(): void; | ||
runInContext(context: IContext, callback: IRunInContextCallback): any; | ||
runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): any; | ||
@@ -12,0 +13,0 @@ runInNullContext(callback: IRunInContextCallback): any; |
@@ -19,4 +19,6 @@ /// <reference types="node" /> | ||
export interface IContextManager { | ||
createContext(req: http.IncomingMessage): IContext; | ||
currentContext(): IContext; | ||
isWithinContext(): boolean; | ||
runInContext(context: IContext, callback: IRunInContextCallback): any; | ||
runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): any; | ||
@@ -23,0 +25,0 @@ runInNullContext(callback: IRunInContextCallback): any; |
@@ -13,4 +13,6 @@ /// <reference types="node" /> | ||
constructor(context?: IContext); | ||
createContext(req: http.IncomingMessage): IContext; | ||
currentContext(): IContext; | ||
isWithinContext(): boolean; | ||
runInContext(context: IContext, callback: IRunInContextCallback): any; | ||
runInNewContext(req: http.IncomingMessage, callback: IRunInContextCallback): any; | ||
@@ -17,0 +19,0 @@ runInNullContext(callback: IRunInContextCallback): any; |
@@ -73,247 +73,279 @@ 'use strict'; | ||
}; | ||
HttpProxy.prototype.setupServerProxy = function (agent, httpModule) { | ||
HttpProxy.prototype.patchCreateServer = function (agent, httpModule) { | ||
var oldCreateServer = httpModule.createServer; | ||
var self = this; | ||
var maxBodySize = HttpHelper_1.HttpHelper.getMaxBodySize(agent.providers.configSettings); | ||
httpModule.createServer = function createServer(options, cb) { | ||
httpModule.createServer = function glimpseHttpCreateServer() { | ||
var args = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
args[_i - 2] = arguments[_i]; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
args[_i] = arguments[_i]; | ||
} | ||
function internalCallback(req, res) { | ||
var rest = []; | ||
var server; | ||
// NOTE: https.createServer() and http.createServer() have different signatures: | ||
// - http.createServer([callback]) | ||
// - https.createServer(options[, callback]) | ||
// | ||
// We can't inspect the callback type because the callback is optional, | ||
// but we can inspect the `options` parameter since it is required for | ||
// HTTPS calls and HTTP calls don't accept an options object | ||
// | ||
// Note that we do *not* pass the callback into the old createServer(); | ||
// it's attached to the `request` event after `on` has been patched. | ||
var cb; | ||
var isHttps = args.length && typeof args[0] === 'object'; | ||
if (isHttps && args.length >= 1 && typeof args[1] === 'function') { | ||
cb = args[1]; | ||
args.splice(1, 1); | ||
} | ||
else if (!isHttps && args.length && typeof args[0] === 'function') { | ||
cb = args[0]; | ||
args.splice(0, 1); | ||
} | ||
server = oldCreateServer.apply(this, args); | ||
// NOTE: Any number of `request` listeners may be attached to the server, | ||
// either attached indirectly via createServer(cb) or attached directly | ||
// via server.on('request', cb). | ||
// | ||
// We must ensure that all of them execute within the same Glimpse context. | ||
// We do this by patching server.on() and wrapping `request` listeners, | ||
// then creating/adding the context. | ||
var oldOn = server.on; | ||
server.on = function glimpseServerOn(eventName, listener) { | ||
var onArgs = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
rest[_i - 2] = arguments[_i]; | ||
onArgs[_i - 2] = arguments[_i]; | ||
} | ||
var requestStartTime = self.getCurrentTimeStamp(); | ||
agent.providers.contextManager.runInNewContext(req, function (context) { | ||
// store context on req/response | ||
HttpHelper_1.HttpHelper.setContext(req, context); | ||
HttpHelper_1.HttpHelper.setContext(res, context); | ||
self.raiseRequestStartEvent(req, res, requestStartTime); | ||
// It is possible in some circumstances that `res.end()` is | ||
// called before the `data` event on the `req` object is | ||
// fired. In this case, we check this flag and send the before | ||
// event immediately before sending the end event. | ||
var requestEndSent = false; | ||
// Note: the User Inspector class was rolled into this one | ||
// because the begin/end events weren't fine graind enough | ||
// to set these headers at the appropriate time. Once this | ||
// module is ported to the new proxy paradigm, this can be | ||
// split back into a separate inspector | ||
// BEGIN code from UserInspector | ||
var requestCookies = HttpHelper_1.RequestHelper.parseCookies(req); | ||
var userId = requestCookies ? requestCookies[SESSION_COOKIE] : undefined; | ||
if (!userId) { | ||
HttpHelper_1.ResponseHelper.setCookie(res, SESSION_COOKIE, GuidHelper_1.GuidHelper.newGuid(false)); | ||
} | ||
// END code from UserInspector | ||
res.setHeader('X-Glimpse-ContextId', context.id); | ||
// General performance note for this implementation: this has been identified | ||
// as a hot path for performance, so there are places where maintainability | ||
// and readability are sacrificed for performance. Specifically, there is | ||
// repeated code in here that could be abstracted into helper methods, but | ||
// would incure the extra stack frame and slow things down | ||
// Note on Buffers. We use Buffers to store body requests, and we | ||
// create new buffers a few times as well. We use the Buffer consructor | ||
// to do this for backwards compatibility reasons, but we should | ||
// migrate away some day. There is a security risk with using the | ||
// Buffer constructor, which is why it's been deprecated. More info: | ||
// https://nodejs.org/api/buffer.html#buffer_buffer_from_buffer_alloc_and_buffer_allocunsafe | ||
// Chunks may be read back as either Buffers or strings. For now we store | ||
// them as an array of chunks, and let inspectors figure out the best way | ||
// to normalize them. | ||
var requestBodyChunks = []; | ||
var requestBodyLength = 0; | ||
var bufferedData = []; | ||
var isBufferingData = true; | ||
var isMultiPartFormData = undefined; | ||
var multiPartFormSummarizer = undefined; | ||
req.on('data', function (chunk) { | ||
agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'data\')', context.id); | ||
// set up a parser to parse out headers if this is a multi-part form request | ||
if (isMultiPartFormData === undefined) { | ||
multiPartFormSummarizer = MultiPartFormSummarizer_1.createMultiPartFormSummarizer(req.headers['content-type']); | ||
if (multiPartFormSummarizer) { | ||
isMultiPartFormData = true; | ||
if (eventName === 'request') { | ||
var oldListener_1 = listener; | ||
listener = function (req, res) { | ||
// NOTE: Glimpse initialization in the user's app is synchronous (e.g. `glimpse.init()`). | ||
// However, there are some Glimpse services that require asynchronous initialization. | ||
// In those cases, we defer initialization until just prior to handling the first | ||
// request (as that's the first asynchronous hook point provided by Node). When | ||
// initialization is complete, we continue processing the request. | ||
agent.providers.deferredInitializationManager.init(function (err) { | ||
if (err) { | ||
throw err; | ||
} | ||
else { | ||
isMultiPartFormData = false; | ||
var context = HttpHelper_1.HttpHelper.getContext(req); | ||
// If no context currently exists, create one and attach to request/response objects... | ||
if (!context) { | ||
context = agent.providers.contextManager.createContext(req); | ||
HttpHelper_1.HttpHelper.setContext(req, context); | ||
HttpHelper_1.HttpHelper.setContext(res, context); | ||
} | ||
} | ||
if (isBufferingData) { | ||
bufferedData.push(chunk); | ||
} | ||
if (isMultiPartFormData) { | ||
multiPartFormSummarizer.addChunk(chunk); | ||
} | ||
var originalChunkLength = chunk.length; | ||
if (requestBodyLength < maxBodySize) { | ||
if (requestBodyLength + originalChunkLength >= maxBodySize) { | ||
chunk = chunk.slice(0, maxBodySize - requestBodyLength); | ||
} | ||
requestBodyChunks.push(chunk); | ||
} | ||
requestBodyLength += originalChunkLength; | ||
}); | ||
var isBufferingEnd = false; | ||
req.on('end', function () { | ||
isBufferingEnd = true; | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'end\')', context.id); | ||
if (!requestEndSent) { | ||
self.raiseRequestEndEvent(req, res, requestBodyChunks, requestBodyLength, requestStartTime, multiPartFormSummarizer ? multiPartFormSummarizer.getParts() : []); | ||
requestEndSent = true; | ||
} | ||
}); | ||
req.on('error', function (err) { | ||
self.errorReportingService.reportError(glimpse_common_1.createHttpServerError(err)); | ||
}); | ||
res.on('error', function (err) { | ||
self.errorReportingService.reportError(glimpse_common_1.createHttpServerError(err)); | ||
}); | ||
// NOTE: We MUST be subscribed to the 'data' and 'end' events PRIOR to patching 'on()'. | ||
var oldOn = req.on; | ||
req.on = function newOn(event, onCallback) { | ||
var _this = this; | ||
var onRest = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
onRest[_i - 2] = arguments[_i]; | ||
} | ||
if (isBufferingData && event === 'data') { | ||
try { | ||
bufferedData.forEach(function (chunk) { | ||
onCallback.call(_this, chunk); | ||
}); | ||
} | ||
finally { | ||
bufferedData = []; | ||
isBufferingData = false; | ||
} | ||
} | ||
else if (isBufferingEnd && event === 'end') { | ||
onCallback.call(this); | ||
} | ||
return oldOn.call.apply(oldOn, [this, event, onCallback].concat(onRest)); | ||
// Run the handler within the context... | ||
return agent.providers.contextManager.runInContext(context, function () { return oldListener_1(req, res); }); | ||
}); | ||
}; | ||
// Note: it's possible to write data using the `end` method as well, | ||
// but that method calls `write` under the hood, and patching both | ||
// leads to a doubly patched write method, which duplicates the body | ||
var responseBodyChunks = []; | ||
var responseBodyLength = 0; | ||
var oldWrite = res.write; | ||
var responseStartTime = undefined; | ||
res.write = function (chunk, encoding) { | ||
var writeArgs = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
writeArgs[_i - 2] = arguments[_i]; | ||
} | ||
if (!responseStartTime) { | ||
responseStartTime = self.getCurrentTimeStamp(); | ||
self.raiseResponseStartEvent(req, res, responseStartTime); | ||
} | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(response.write())', context.id); | ||
// Short circuit if we're not actually writing anything | ||
if (typeof chunk === 'function' || typeof chunk === 'undefined') { | ||
return oldWrite.call.apply(oldWrite, [this, chunk, encoding].concat(writeArgs)); | ||
} | ||
// If we don't have the necessary information to normalize | ||
// to a string, the underlying API will throw, so we short | ||
// circuit here and call the underlying API | ||
if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) { | ||
return oldWrite.call.apply(oldWrite, [this, chunk, encoding].concat(writeArgs)); | ||
} | ||
// Save part or all of the chunk to the set of chunks, | ||
// truncating if necessary to keep the set under the | ||
// max body size | ||
var originalChunkLength = chunk.length; | ||
var normalizedChunk = chunk; | ||
if (responseBodyLength < maxBodySize) { | ||
if (responseBodyLength + originalChunkLength >= maxBodySize) { | ||
normalizedChunk = normalizedChunk.slice(0, maxBodySize - responseBodyLength); | ||
} | ||
responseBodyChunks.push(normalizedChunk); | ||
} | ||
responseBodyLength += originalChunkLength; | ||
return oldWrite.call.apply(oldWrite, [this, chunk, encoding].concat(writeArgs)); | ||
}; | ||
// We override the setHeader method so we can intercept the | ||
// content-type and set the request ID header if this is the | ||
// first request for the page. We will know if this request | ||
// was the first request for the page if it's a) a request | ||
// for HTML and b) it's not an AJAX request because it's not | ||
// possible to request HTML content after the initial request | ||
// *except* via AJAX. | ||
var oldSetHeader = res.setHeader; | ||
res.setHeader = function setHeader(name, value) { | ||
var setHeaderArgs = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
setHeaderArgs[_i - 2] = arguments[_i]; | ||
} | ||
oldSetHeader.call.apply(oldSetHeader, [this, name, value].concat(setHeaderArgs)); | ||
if (name.toLowerCase() === 'content-type' && value.indexOf('text/html') === 0 && HttpHelper_1.RequestHelper.header(req, IS_AJAX_HEADER) !== 'true') { | ||
HttpHelper_1.ResponseHelper.setCookie(res, REQUEST_ID_COOKIE, context.id); | ||
} | ||
}; | ||
res.on('finish', function () { | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'finish\')', context.id); | ||
if (!requestEndSent) { | ||
// If we got here, it means that the `res.end()` method | ||
// was called before the request `end` event was fired. | ||
// This could happen for a variety of reasons, mostly | ||
// when the user calls `res.end` before the request has | ||
// finished flushing. Most often, this happens because | ||
// the body has not been recieved, but we try and send | ||
// whatever body we have receieved so far. | ||
self.raiseRequestEndEvent(req, res, requestBodyChunks, requestBodyLength, requestStartTime, multiPartFormSummarizer ? multiPartFormSummarizer.getParts() : []); | ||
requestEndSent = true; | ||
// We check to see if content length was set, and if it | ||
// was and we haven't seen that much of the body yet, | ||
// we report an error that not all of the body was captured | ||
var contentLength = HttpHelper_1.RequestHelper.header(req, 'content-length'); | ||
if (contentLength && contentLength > requestBodyLength && self.errorReportingService) { | ||
self.errorReportingService.reportError(glimpse_common_1.createHttpServerEarlyRequestTerminationError(req.url)); | ||
} | ||
} | ||
responseStartTime = responseStartTime || self.getCurrentTimeStamp(); | ||
self.raiseResponseEndEvent(req, res, responseBodyChunks, responseBodyLength, responseStartTime); | ||
}); | ||
if (cb) { | ||
cb.apply(void 0, [req, res].concat(rest)); | ||
} | ||
return oldOn.call.apply(oldOn, [this, eventName, listener].concat(onArgs)); | ||
}; | ||
// Attach HttpProxy-specific logic to `request` event (which wraps any callback passed to createServer())... | ||
server.on('request', self.setupServerProxy(agent, cb)); | ||
return server; | ||
}; | ||
}; | ||
HttpProxy.prototype.setupServerProxy = function (agent, cb) { | ||
var self = this; | ||
var maxBodySize = HttpHelper_1.HttpHelper.getMaxBodySize(agent.providers.configSettings); | ||
return function internalCallback(req, res) { | ||
var rest = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
rest[_i - 2] = arguments[_i]; | ||
} | ||
var context = HttpHelper_1.HttpHelper.getContext(req); | ||
var requestStartTime = self.getCurrentTimeStamp(); | ||
self.raiseRequestStartEvent(req, res, requestStartTime); | ||
// It is possible in some circumstances that `res.end()` is | ||
// called before the `data` event on the `req` object is | ||
// fired. In this case, we check this flag and send the before | ||
// event immediately before sending the end event. | ||
var requestEndSent = false; | ||
// Note: the User Inspector class was rolled into this one | ||
// because the begin/end events weren't fine graind enough | ||
// to set these headers at the appropriate time. Once this | ||
// module is ported to the new proxy paradigm, this can be | ||
// split back into a separate inspector | ||
// BEGIN code from UserInspector | ||
var requestCookies = HttpHelper_1.RequestHelper.parseCookies(req); | ||
var userId = requestCookies ? requestCookies[SESSION_COOKIE] : undefined; | ||
if (!userId) { | ||
HttpHelper_1.ResponseHelper.setCookie(res, SESSION_COOKIE, GuidHelper_1.GuidHelper.newGuid(false)); | ||
} | ||
// END code from UserInspector | ||
res.setHeader('X-Glimpse-ContextId', context.id); | ||
// General performance note for this implementation: this has been identified | ||
// as a hot path for performance, so there are places where maintainability | ||
// and readability are sacrificed for performance. Specifically, there is | ||
// repeated code in here that could be abstracted into helper methods, but | ||
// would incure the extra stack frame and slow things down | ||
// Note on Buffers. We use Buffers to store body requests, and we | ||
// create new buffers a few times as well. We use the Buffer consructor | ||
// to do this for backwards compatibility reasons, but we should | ||
// migrate away some day. There is a security risk with using the | ||
// Buffer constructor, which is why it's been deprecated. More info: | ||
// https://nodejs.org/api/buffer.html#buffer_buffer_from_buffer_alloc_and_buffer_allocunsafe | ||
// Chunks may be read back as either Buffers or strings. For now we store | ||
// them as an array of chunks, and let inspectors figure out the best way | ||
// to normalize them. | ||
var requestBodyChunks = []; | ||
var requestBodyLength = 0; | ||
var bufferedData = []; | ||
var isBufferingData = true; | ||
var isMultiPartFormData = undefined; | ||
var multiPartFormSummarizer = undefined; | ||
req.on('data', function (chunk) { | ||
agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'data\')', context.id); | ||
// set up a parser to parse out headers if this is a multi-part form request | ||
if (isMultiPartFormData === undefined) { | ||
multiPartFormSummarizer = MultiPartFormSummarizer_1.createMultiPartFormSummarizer(req.headers['content-type']); | ||
if (multiPartFormSummarizer) { | ||
isMultiPartFormData = true; | ||
} | ||
}); | ||
} | ||
; | ||
// NOTE: Glimpse initialization in the user's app is synchronous (e.g. `glimpse.init()`). | ||
// However, there are some Glimpse services that require asynchronous initialization. | ||
// In those cases, we defer initialization until just prior to handling the first | ||
// request (as that's the first asynchronous hook point provided by Node). When | ||
// initialization is complete, we continue processing the request. | ||
function deferredCallback(req, res) { | ||
var rest = []; | ||
else { | ||
isMultiPartFormData = false; | ||
} | ||
} | ||
if (isBufferingData) { | ||
bufferedData.push(chunk); | ||
} | ||
if (isMultiPartFormData) { | ||
multiPartFormSummarizer.addChunk(chunk); | ||
} | ||
var originalChunkLength = chunk.length; | ||
if (requestBodyLength < maxBodySize) { | ||
if (requestBodyLength + originalChunkLength >= maxBodySize) { | ||
chunk = chunk.slice(0, maxBodySize - requestBodyLength); | ||
} | ||
requestBodyChunks.push(chunk); | ||
} | ||
requestBodyLength += originalChunkLength; | ||
}); | ||
var isBufferingEnd = false; | ||
req.on('end', function () { | ||
isBufferingEnd = true; | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'end\')', context.id); | ||
if (!requestEndSent) { | ||
self.raiseRequestEndEvent(req, res, requestBodyChunks, requestBodyLength, requestStartTime, multiPartFormSummarizer ? multiPartFormSummarizer.getParts() : []); | ||
requestEndSent = true; | ||
} | ||
}); | ||
req.on('error', function (err) { | ||
self.errorReportingService.reportError(glimpse_common_1.createHttpServerError(err)); | ||
}); | ||
res.on('error', function (err) { | ||
self.errorReportingService.reportError(glimpse_common_1.createHttpServerError(err)); | ||
}); | ||
// NOTE: We MUST be subscribed to the 'data' and 'end' events PRIOR to patching 'on()'. | ||
var oldOn = req.on; | ||
req.on = function newOn(event, onCallback) { | ||
var _this = this; | ||
var onRest = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
rest[_i - 2] = arguments[_i]; | ||
onRest[_i - 2] = arguments[_i]; | ||
} | ||
agent.providers.deferredInitializationManager.init(function (err) { | ||
if (err) { | ||
throw err; | ||
if (isBufferingData && event === 'data') { | ||
try { | ||
bufferedData.forEach(function (chunk) { | ||
onCallback.call(_this, chunk); | ||
}); | ||
} | ||
internalCallback.apply(void 0, [req, res].concat(rest)); | ||
}); | ||
finally { | ||
bufferedData = []; | ||
isBufferingData = false; | ||
} | ||
} | ||
else if (isBufferingEnd && event === 'end') { | ||
onCallback.call(this); | ||
} | ||
return oldOn.call.apply(oldOn, [this, event, onCallback].concat(onRest)); | ||
}; | ||
// Note: it's possible to write data using the `end` method as well, | ||
// but that method calls `write` under the hood, and patching both | ||
// leads to a doubly patched write method, which duplicates the body | ||
var responseBodyChunks = []; | ||
var responseBodyLength = 0; | ||
var oldWrite = res.write; | ||
var responseStartTime = undefined; | ||
res.write = function (chunk, encoding) { | ||
var writeArgs = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
writeArgs[_i - 2] = arguments[_i]; | ||
} | ||
if (!responseStartTime) { | ||
responseStartTime = self.getCurrentTimeStamp(); | ||
self.raiseResponseStartEvent(req, res, responseStartTime); | ||
} | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(response.write())', context.id); | ||
// Short circuit if we're not actually writing anything | ||
if (typeof chunk === 'function' || typeof chunk === 'undefined') { | ||
return oldWrite.call.apply(oldWrite, [this, chunk, encoding].concat(writeArgs)); | ||
} | ||
// If we don't have the necessary information to normalize | ||
// to a string, the underlying API will throw, so we short | ||
// circuit here and call the underlying API | ||
if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) { | ||
return oldWrite.call.apply(oldWrite, [this, chunk, encoding].concat(writeArgs)); | ||
} | ||
// Save part or all of the chunk to the set of chunks, | ||
// truncating if necessary to keep the set under the | ||
// max body size | ||
var originalChunkLength = chunk.length; | ||
var normalizedChunk = chunk; | ||
if (responseBodyLength < maxBodySize) { | ||
if (responseBodyLength + originalChunkLength >= maxBodySize) { | ||
normalizedChunk = normalizedChunk.slice(0, maxBodySize - responseBodyLength); | ||
} | ||
responseBodyChunks.push(normalizedChunk); | ||
} | ||
responseBodyLength += originalChunkLength; | ||
return oldWrite.call.apply(oldWrite, [this, chunk, encoding].concat(writeArgs)); | ||
}; | ||
// We override the setHeader method so we can intercept the | ||
// content-type and set the request ID header if this is the | ||
// first request for the page. We will know if this request | ||
// was the first request for the page if it's a) a request | ||
// for HTML and b) it's not an AJAX request because it's not | ||
// possible to request HTML content after the initial request | ||
// *except* via AJAX. | ||
var oldSetHeader = res.setHeader; | ||
res.setHeader = function setHeader(name, value) { | ||
var setHeaderArgs = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
setHeaderArgs[_i - 2] = arguments[_i]; | ||
} | ||
oldSetHeader.call.apply(oldSetHeader, [this, name, value].concat(setHeaderArgs)); | ||
if (name.toLowerCase() === 'content-type' && value.indexOf('text/html') === 0 && HttpHelper_1.RequestHelper.header(req, IS_AJAX_HEADER) !== 'true') { | ||
HttpHelper_1.ResponseHelper.setCookie(res, REQUEST_ID_COOKIE, context.id); | ||
} | ||
}; | ||
res.on('finish', function () { | ||
// TODO: renable this check when we have an effective context tracking implementation in place | ||
//agent.providers.contextManager.checkContextID('HttpProxy::(request - on(\'finish\')', context.id); | ||
if (!requestEndSent) { | ||
// If we got here, it means that the `res.end()` method | ||
// was called before the request `end` event was fired. | ||
// This could happen for a variety of reasons, mostly | ||
// when the user calls `res.end` before the request has | ||
// finished flushing. Most often, this happens because | ||
// the body has not been recieved, but we try and send | ||
// whatever body we have receieved so far. | ||
self.raiseRequestEndEvent(req, res, requestBodyChunks, requestBodyLength, requestStartTime, multiPartFormSummarizer ? multiPartFormSummarizer.getParts() : []); | ||
requestEndSent = true; | ||
// We check to see if content length was set, and if it | ||
// was and we haven't seen that much of the body yet, | ||
// we report an error that not all of the body was captured | ||
var contentLength = HttpHelper_1.RequestHelper.header(req, 'content-length'); | ||
if (contentLength && contentLength > requestBodyLength && self.errorReportingService) { | ||
self.errorReportingService.reportError(glimpse_common_1.createHttpServerEarlyRequestTerminationError(req.url)); | ||
} | ||
} | ||
responseStartTime = responseStartTime || self.getCurrentTimeStamp(); | ||
self.raiseResponseEndEvent(req, res, responseBodyChunks, responseBodyLength, responseStartTime); | ||
}); | ||
if (cb) { | ||
cb.apply(void 0, [req, res].concat(rest)); | ||
} | ||
// Note: https.createServer and http.createServer have different signatures: | ||
// http.createServer([callback]) | ||
// https.createServer(options[, callback]) | ||
// We can't inspect the callback type because the callback is optional, | ||
// but we can inspect the `options` parameter since it is required for | ||
// HTTPS calls and HTTP calls don't accept an options object | ||
if (typeof options !== 'object') { | ||
cb = options; | ||
return oldCreateServer.call.apply(oldCreateServer, [this, deferredCallback].concat(args)); | ||
} | ||
else { | ||
return oldCreateServer.call.apply(oldCreateServer, [this, options, deferredCallback].concat(args)); | ||
} | ||
}; | ||
@@ -342,3 +374,3 @@ }; | ||
this.proxiedModules.push(httpModule); | ||
this.setupServerProxy(agent, httpModule); | ||
this.patchCreateServer(agent, httpModule); | ||
this.setupInspectorExtensions(agent); | ||
@@ -345,0 +377,0 @@ return httpModule; |
@@ -16,2 +16,6 @@ "use strict"; | ||
this.mapCache = {}; | ||
/** | ||
* keep track of any strings we can't parse so we only report an error once per string | ||
*/ | ||
this.reportedErrors = {}; | ||
this.agentRoot = path.resolve(path.join(__dirname, '../..')); | ||
@@ -167,11 +171,16 @@ this.errorReportingService = errorService; | ||
} | ||
else if (openParen && closeParen && source.substring(openParen + 1, closeParen) === 'native') { | ||
// case ' at Array.forEach (native)' | ||
functionName = source.substring(7, openParen).trim(); | ||
fileName = 'native'; | ||
line = '0'; | ||
column = '0'; | ||
else if (openParen && closeParen) { | ||
var tryFileName = source.substring(openParen + 1, closeParen); | ||
if (tryFileName === 'native' || tryFileName === '<anonymous>') { | ||
// case ' at Array.forEach (native)' or ' at callFn.next (<anonymous>)' | ||
functionName = source.substring(7, openParen).trim(); | ||
fileName = tryFileName; | ||
line = '0'; | ||
column = '0'; | ||
} | ||
} | ||
else { | ||
if (this.errorReportingService) { | ||
// if filename is undefined, we failed to parse the frame | ||
if (!fileName) { | ||
if (this.errorReportingService && !this.reportedErrors[source]) { | ||
this.reportedErrors[source] = true; | ||
this.errorReportingService.reportError(glimpse_common_1.createStackHelperUnsupportedStackFrameFormat(source)); | ||
@@ -178,0 +187,0 @@ } |
@@ -69,4 +69,3 @@ 'use strict'; | ||
// tslint:disable-next-line:no-any | ||
ContextManagerAsyncTrack.prototype.runInNewContext = function (req, callback) { | ||
var context = this.createContext(req); | ||
ContextManagerAsyncTrack.prototype.runInContext = function (context, callback) { | ||
var callbackWrapper = function callbackWrapper() { | ||
@@ -78,8 +77,8 @@ return callback(context); | ||
// tslint:disable-next-line:no-any | ||
ContextManagerAsyncTrack.prototype.runInNewContext = function (req, callback) { | ||
return this.runInContext(this.createContext(req), callback); | ||
}; | ||
// tslint:disable-next-line:no-any | ||
ContextManagerAsyncTrack.prototype.runInNullContext = function (callback) { | ||
var context = undefined; | ||
var callbackWrapper = function callbackWrapper() { | ||
return callback(context); | ||
}; | ||
return this.asyncTrack.runInContext(callbackWrapper, this.createAsyncState(undefined)); | ||
return this.runInContext(undefined, callback); | ||
}; | ||
@@ -86,0 +85,0 @@ ContextManagerAsyncTrack.prototype.wrapInCurrentContext = function (callback) { |
@@ -27,2 +27,5 @@ 'use strict'; | ||
}; | ||
ContextManagerBase.prototype.runInContext = function (context, callback) { | ||
throw new Error('please override'); | ||
}; | ||
ContextManagerBase.prototype.runInNewContext = function (req, callback) { | ||
@@ -29,0 +32,0 @@ throw new Error('please override'); |
@@ -28,6 +28,5 @@ 'use strict'; | ||
// tslint:disable-next-line:no-any | ||
ContextManagerContinuationLocalStorage.prototype.runInNewContext = function (req, callback) { | ||
ContextManagerContinuationLocalStorage.prototype.runInContext = function (context, callback) { | ||
var _this = this; | ||
var wrapper = function () { | ||
var context = _this.createContext(req); | ||
_this._namespace.set(ContextManagerContinuationLocalStorage.GLIMPSE_CONTEXT, context); | ||
@@ -41,11 +40,9 @@ return callback(context); | ||
// tslint:disable-next-line:no-any | ||
ContextManagerContinuationLocalStorage.prototype.runInNewContext = function (req, callback) { | ||
return this.runInContext(this.createContext(req), callback); | ||
}; | ||
; | ||
// tslint:disable-next-line:no-any | ||
ContextManagerContinuationLocalStorage.prototype.runInNullContext = function (callback) { | ||
var _this = this; | ||
var wrapper = function () { | ||
var context = undefined; | ||
_this._namespace.set(ContextManagerContinuationLocalStorage.GLIMPSE_CONTEXT, context); | ||
return callback(context); | ||
}; | ||
var boundFunction = this._namespace.bind(wrapper, this._namespace.createContext()); | ||
return boundFunction(); | ||
return this.runInContext(undefined, callback); | ||
}; | ||
@@ -52,0 +49,0 @@ ; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1431171
18954
42
+ Added@glimpse/glimpse-common@0.20.9(transitive)
- Removed@glimpse/glimpse-common@0.20.8(transitive)