Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

newrelic

Package Overview
Dependencies
Maintainers
1
Versions
383
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

newrelic - npm Package Compare versions

Comparing version 0.9.12-92 to 0.9.13-101

lib/util/deep-equal.js

2

lib/agent.js

@@ -160,3 +160,3 @@ 'use strict';

Agent.prototype.updateNormalizerRules = function (response) {
this.normalizer.parseMetricRules(response);
this.normalizer.load(response);
};

@@ -163,0 +163,0 @@

@@ -13,5 +13,19 @@ 'use strict';

var PROTOCOL_VERSION = 9;
/*
* CONSTANTS
*/
var PROTOCOL_VERSION = 9
, RESPONSE_VALUE_NAME = 'return_value'
, RUN_ID_NAME = 'agent_run_id'
, RAW_METHOD_PATH = '/agent_listener/invoke_raw_method'
// https://hudson.newrelic.com/job/collector-master/javadoc/com/nr/servlet/AgentListener.html
, USER_AGENT_FORMAT = "NewRelic-NodeAgent/%s (nodejs %s %s-%s)"
, ENCODING_HEADER = 'CONTENT-ENCODING'
, CONTENT_TYPE_HEADER = 'Content-Type'
, DEFAULT_ENCODING = 'identity'
, COMPRESSED_ENCODING = 'deflate'
, DEFAULT_CONTENT_TYPE = 'application/json'
, COMPRESSED_CONTENT_TYPE = 'application/octet-stream'
;
// FIXME support proxies
// TODO add configurable timeout to connections

@@ -24,156 +38,236 @@ function DataSender(config, agentRunId) {

this._url = url.format({
pathname : '/agent_listener/invoke_raw_method',
query : {
marshal_format : 'json',
protocol_version : PROTOCOL_VERSION,
license_key : this.config.license_key
}
});
this.on('error', function (method, error) {
var hostname = this.config.proxy_host || this.config.host;
if (typeof error === 'object') {
if (error.error_type !== 'NewRelic::Agent::ForceRestartException') {
logger.debug("Attempting to send data to collector %s failed:", hostname);
logger.debug(error);
}
}
else {
logger.debug("Attempting to send data to collector %s failed: %j", hostname, error);
}
}.bind(this));
this.on('error', this.onError.bind(this));
}
util.inherits(DataSender, events.EventEmitter);
DataSender.prototype.getUserAgent = function () {
// as per https://hudson.newrelic.com/job/collector-master/javadoc/com/nr/servlet/AgentListener.html
return util.format("NewRelic-NodeAgent/%s (nodejs %s %s-%s)",
this.config.version,
process.versions.node,
process.platform,
process.arch);
};
/**
* The primary interface to DataSender objects. If you're calling anything on
* DataSender objects aside from invokeMethod (and you're not writing test
* code), something is probably awry.
*
* @param string message The type of message you want to send the collector.
* @param object data Serializable data to be sent.
*/
DataSender.prototype.invokeMethod = function (message, body) {
if (!message) throw new Error("Can't send the collector a message without a message type.");
DataSender.prototype.canonicalizeURL = function (url) {
if (this.config.proxy_host) {
return 'http://' + this.config.host + ':' + this.config.port + url;
var data = JSON.stringify(body || []);
if (this.shouldCompress(data)) {
logger.trace("Data[%s] (COMPRESSED): %s", message, (data || "(no data)"));
zlib.deflate(data, function (err, deflated) {
if (err) return logger.verbose(err, "Error compressing JSON for delivery. Not sending.");
var headers = this.getHeaders(deflated, true);
this.postToCollector(message, headers, deflated);
}.bind(this));
}
else {
return url;
logger.trace("Data[%s]: %s", message, (data || "(no data)"));
var headers = this.getHeaders(data, false);
// ensure that invokeMethod is always asynchronous
process.nextTick(this.postToCollector.bind(this, message, headers, data));
}
};
DataSender.prototype.createHeaders = function (length, compressed) {
return {
"CONTENT-ENCODING" : compressed ? 'deflate' : 'identity',
"Content-Length" : length,
"Connection" : "Keep-Alive",
"host" : this.config.host,
"Content-Type" : compressed ? 'application/octet-stream' : 'application/json',
"User-Agent" : this.getUserAgent()
};
};
/**
* Send a message to the collector and set up the sender to handle the
* result.
*
* @param string message The message type being sent to the collector.
* @param object headers The headers for this request.
* @param string data The encoded data, ready for delivery.
*/
DataSender.prototype.postToCollector = function (message, headers, data) {
var port = this.config.proxy_port || this.config.port
, host = this.config.proxy_host || this.config.host
, urlPath = this.getURL(message)
;
DataSender.prototype.createRequest = function (method, url, headers) {
logger.debug("Posting %s message to %s:%s at %s.", message, host, port, urlPath);
var request = http.request({
__NR__connection : true, // who measures the metrics measurer?
method : 'POST',
port : this.config.proxy_port || this.config.port,
host : this.config.proxy_host || this.config.host,
path : this.canonicalizeURL(url),
port : port,
host : host,
path : urlPath,
headers : headers
});
request.on('error', function (error) {
logger.info(error, "Error invoking %s method.", method);
this.emit('error', method, error);
}.bind(this));
request.on('error', this.onError.bind(this, message));
request.on('response', this.onCollectorResponse.bind(this, message));
request.on('response', function (response) {
var VALUE = 'return_value';
return request.end(data);
};
if (response.statusCode < 200 || response.statusCode >= 300) {
return this.emit('error', method, response.statusCode);
}
/**
* ForceRestartExceptions aren't actually errors, don't log them as such.
*/
DataSender.prototype.onError = function (message, error) {
var hostname = this.config.proxy_host || this.config.host;
if (error.error_type !== 'NewRelic::Agent::ForceRestartException') {
logger.debug(error,
"Attempting to send %s to collector %s failed:",
message,
hostname);
}
};
// if the encoding isn't explicitly set on the response, the chunks will
// be Buffers and not strings.
response.setEncoding('utf8');
response.pipe(new StreamSink(function (error, body) {
if (error) return this.emit('error', method, response);
/**
* Pipe the data from the collector to a response handler.
*
* @param string message The message being sent to the collector.
*/
DataSender.prototype.onCollectorResponse = function (message, response) {
response.on('end', function () {
logger.debug("Finished receiving data back from the collector for %s.", message);
});
var message = JSON.parse(body);
if (response.statusCode < 200 || response.statusCode >= 300) {
logger.error("Got %s as a response code from the collector.",
response.statusCode);
return this.emit('error',
message,
new Error(util.format("Got HTTP %s in response to %s.",
response.statusCode,
message)));
}
// can be super verbose, but save as a raw object anyway
logger.trace({response : message}, "parsed response from collector:");
if (message.exception) return this.emit('error', method, message.exception);
response.setEncoding('utf8');
response.pipe(new StreamSink(this.handleMessage.bind(this, message)));
};
// if we get messages back from the collector, be polite and pass them along
if (message[VALUE] && message[VALUE].messages) {
var messages = message[VALUE].messages;
messages.forEach(function (message) { logger.info(message.message); });
}
/**
* Responses from the collector follow the convention:
*
* { exception : { <exception JSON> },
* return_value : {
* messages : [ <messages> ],
* <other stuff>
* }
* }
*
* Exceptions are emitted as-is, as errors.
* Anything associated with return_value is emitted as a response on
* the DataSender.
*
* @param string message The message type sent to the collector.
* @param error error The error, if any, resulting from decoding the
* response.
* @param string body The body of the response.
*/
DataSender.prototype.handleMessage = function (message, error, body) {
if (error) return this.emit('error', message, error);
this.emit('response', message[VALUE]);
}.bind(this)));
}.bind(this));
var json = JSON.parse(body);
// can be super verbose, but useful for debugging
logger.trace({response : json}, "parsed response from collector:");
return request;
};
// If we get messages back from the collector, be polite and pass them along.
var returned = json[RESPONSE_VALUE_NAME];
if (returned && returned.messages) {
returned.messages.forEach(function (element) {
logger.info(element.message);
});
}
DataSender.prototype.sendPlaintext = function sendPlainText(method, url, data) {
var contentLength = Buffer.byteLength(data, 'utf8')
, headers = this.createHeaders(contentLength)
;
/* If there's an exception, wait to return it until any messages have
* been passed along.
*/
if (json.exception) {
return this.emit('error', message, json.exception);
}
logger.trace("Headers: %j", headers);
logger.debug("Data[%s]: %s", method, (data || "(no data)"));
return this.emit('response', returned);
};
var request = this.createRequest(method, url, headers);
request.write(data);
request.end();
/**
* See the constants list for the format string (and the URL that explains it).
*/
DataSender.prototype.getUserAgent = function () {
return util.format(USER_AGENT_FORMAT,
this.config.version,
process.versions.node,
process.platform,
process.arch);
};
DataSender.prototype.sendCompressed = function sendCompressed(method, url, data) {
zlib.deflate(data, function (err, deflated) {
if (err) return logger.verboe(err, "Error compressing JSON for delivery.");
/**
* This method implies proxy support, but it's completely untested
* (and mostly undocumented in the config).
*
* FIXME tested, more robust proxy support
* FIXME use the newer "RESTful" URLs
*
* @param string message The message type sent to the collector.
*
* @returns string The URL path to be POSTed to.
*/
DataSender.prototype.getURL = function (message) {
var query = {
marshal_format : 'json',
protocol_version : PROTOCOL_VERSION,
license_key : this.config.license_key,
method : message
};
var contentLength = deflated.length
, headers = this.createHeaders(contentLength, true) // cmprssd plz
;
if (this.agentRunId) query[RUN_ID_NAME] = this.agentRunId;
logger.debug("Headers: %s", util.inspect(headers));
logger.debug("Data[%s] (COMPRESSED): %s", method, (data || "(no data)"));
var formatted = url.format({
pathname : RAW_METHOD_PATH,
query : query
});
var request = this.createRequest(method, url, headers);
request.write(deflated);
request.end();
}.bind(this));
if (this.config.proxy_host) {
return 'http://' + this.config.host + ':' + this.config.port + formatted;
}
else {
return formatted;
}
};
DataSender.prototype.send = function (method, url, params) {
logger.debug("Posting to %s.", url);
/**
*
* @param string data (Potentially compressed) data to be sent to
* collector.
* @param boolean compressed Whether the data are compressed.
*/
DataSender.prototype.getHeaders = function (data, compressed) {
var length = Buffer.byteLength(data, 'utf8')
, agent = this.getUserAgent()
;
if (!params) params = [];
var data = JSON.stringify(params);
var headers = {
'User-Agent' : agent,
'Connection' : 'Keep-Alive',
// select the virtual host on the server end
'host' : this.config.host,
'Content-Length' : length,
};
var contentLength = Buffer.byteLength(data, 'utf8');
if (contentLength > 65536) {
this.sendCompressed(method, url, data);
if (compressed) {
headers[ENCODING_HEADER] = COMPRESSED_ENCODING;
headers[CONTENT_TYPE_HEADER] = COMPRESSED_CONTENT_TYPE;
}
else {
this.sendPlaintext(method, url, data);
headers[ENCODING_HEADER] = DEFAULT_ENCODING;
headers[CONTENT_TYPE_HEADER] = DEFAULT_CONTENT_TYPE;
}
return headers;
};
DataSender.prototype.invokeMethod = function (method, params) {
var url = this._url + "&method=" + method;
if (this.agentRunId) url += "&agent_run_id=" + this.agentRunId;
this.send(method, url, params);
/**
* FLN pretty much decided on his own recognizance that 64K was a good point
* at which to compress a server response. There's only a loose consensus that
* the threshold should probably be much higher than this, if only to keep the
* load on the collector down.
*
* FIXME: come up with a better heuristic
*/
DataSender.prototype.shouldCompress = function (data) {
return data && Buffer.byteLength(data, 'utf8') > 65536;
};
module.exports = DataSender;
'use strict';
var path = require('path')
, util = require('util')
, logger = require(path.join(__dirname, '..', 'logger')).child({component : 'metric_normalizer'})
, Rule = require(path.join(__dirname, 'normalizer', 'rule'))
var path = require('path')
, util = require('util')
, logger = require(path.join(__dirname, '..', 'logger')).child({component : 'metric_normalizer'})
, deepEqual = require(path.join(__dirname, '..', 'util', 'deep-equal'))
, Rule = require(path.join(__dirname, 'normalizer', 'rule'))
;
function MetricNormalizer(rules) {
if (rules) this.parseMetricRules(rules);
/**
* The collector keeps track of rules that should be applied to metric names,
* and sends these rules to the agent at connection time. These rules can
* either change the name of the metric or indicate that metrics associated with
* this name (which is generally a URL path) should be ignored altogether.
*/
function MetricNormalizer(json) {
if (json) this.load(json);
}
MetricNormalizer.prototype.parseMetricRules = function (connectResponse) {
if (connectResponse && connectResponse.url_rules) {
logger.debug("Received %d metric normalization rule(s)",
connectResponse.url_rules.length);
/**
* Convert the raw, deserialized JSON response into a set of
* NormalizationRules.
*
* FIXME: dedupe the rule list after sorting.
*
* @param object json The deserialized JSON response sent on collector
* connection.
*/
MetricNormalizer.prototype.load = function (json) {
if (json && json.url_rules) {
var raw = json.url_rules;
logger.debug("Received %d metric normalization rule(s)", raw.length);
if (!this.rules) this.rules = [];
connectResponse.url_rules.forEach(function (ruleJSON) {
this.rules.push(new Rule(ruleJSON));
raw.forEach(function (json) {
var rule = new Rule(json);
// no need to add the same rule twice
if (!this.rules.some(function (r) { return deepEqual(r, rule); })) {
this.rules.push(rule);
}
}.bind(this));
// I (FLN) always forget this, so making a note:
// JS sort is always IN-PLACE.
this.rules.sort(function (a, b) {
return a.precedence - b.precedence;
});
/* I (FLN) always forget this, so making a note: JS sort is always
* IN-PLACE, even though it returns the sorted array.
*/
this.rules.sort(function (a, b) { return a.precedence - b.precedence; });
logger.debug("Normalized to %s metric normalization rule(s).", this.rules.length);
}
};
MetricNormalizer.prototype.normalizeUrl = function (url) {
if (!this.rules) return null;
/**
* Returns an object with these properties:
*
* 1. name: the raw name
* 2. normalized: the normalized name (if applicable)
* 3. ignore: present and true if the matched rule says to ignore matching names
* 4. terminal: present and true if the matched rule terminated evaluation
*/
MetricNormalizer.prototype.normalize = function (name) {
var result = {name : name};
var normalized = url;
var isNormalized = false;
if (!this.rules || this.rules.length === 0) return result;
var last = name
, normalized
;
for (var i = 0; i < this.rules.length; i++) {
if (this.rules[i].matches(normalized)) {
/*
* It's possible for normalization rules to match without transforming.
*
* Don't assume that it's required for the URL to actually change
* for normalization to have taken place.
*/
isNormalized = true;
normalized = this.rules[i].apply(normalized);
// assume that terminate_chain only applies upon match
if (this.rules[i].isTerminal) break;
var rule = this.rules[i];
if (rule.matches(last)) {
result.normalized = rule.apply(last);
if (rule.ignore) {
result.ignore = true;
delete result.normalized;
logger.trace(rule, "Ignoring %s because of rule:", name);
break;
}
logger.trace(rule, "Normalized %s to %s because of rule:",
last, result.normalized);
if (rule.isTerminal) {
result.terminal = true;
logger.trace(rule, "Terminating normalization because of rule:");
break;
}
last = result.normalized;
}
}
if (!isNormalized) return null;
return normalized;
return result;
};
module.exports = MetricNormalizer;
'use strict';
var path = require('path')
var path = require('path')
, logger = require(path.join(__dirname, '..', '..', 'logger')).child({component : 'normalizer_rule'})

@@ -32,3 +32,2 @@ ;

this.isTerminal = json.terminate_chain || false;
this.patternString = json.match_expression || '^$';
this.replacement = replaceReplacer(json.replacement || '$0');

@@ -43,4 +42,4 @@ this.replaceAll = json.replace_all || false;

try {
this.pattern = new RegExp(this.patternString, modifiers);
logger.trace("Parsed URL normalization rule with pattern %s.", this.pattern);
this.pattern = new RegExp(json.match_expression || '^$', modifiers);
logger.trace("Loaded normalization rule: %j", this);
}

@@ -91,2 +90,8 @@ catch (error) {

.map(function (segment) {
/* String.split will return empty segments when the path has a leading
* slash or contains a run of slashes. Don't inadvertently substitute or
* drop these empty segments, or the normalized path will be wrong.
*/
if (segment === "") return segment;
return segment.replace(this.pattern, this.replacement);

@@ -93,0 +98,0 @@ }.bind(this))

@@ -115,5 +115,7 @@ 'use strict';

if (this.trace && (duration || duration === 0)) this.trace.setDurationInMillis(duration);
if (this.trace && (duration || duration === 0)) {
this.trace.setDurationInMillis(duration);
}
var partialName;
var name, partialName;
if (statusCode === 414 || // Request-URI Too Long

@@ -131,5 +133,5 @@ (statusCode >= 400 && statusCode < 405)) {

var normalizedUrl = this.normalizer.normalizeUrl(this.url);
if (normalizedUrl) {
partialName = 'NormalizedUri' + normalizedUrl;
name = this.normalizer.normalize(this.url);
if (name.normalized) {
partialName = 'NormalizedUri' + name.normalized;
}

@@ -141,20 +143,26 @@ else {

var metrics = this.metrics;
metrics.measureDurationUnscoped('WebTransaction', duration);
metrics.measureDurationUnscoped('HttpDispatcher', duration);
// normalization rules tell us to ignore certain metrics
if (name && name.ignore) {
this.ignore = true;
}
else {
var metrics = this.metrics;
metrics.measureDurationUnscoped('WebTransaction', duration);
metrics.measureDurationUnscoped('HttpDispatcher', duration);
this.scope = "WebTransaction/" + partialName;
// var maxDuration = Math.max(0, duration - this.totalExclusive);
var maxDuration = Math.max(0, exclusiveDuration);
metrics.measureDurationUnscoped(this.scope, duration, maxDuration);
this.scope = "WebTransaction/" + partialName;
// var maxDuration = Math.max(0, duration - this.totalExclusive);
var maxDuration = Math.max(0, exclusiveDuration);
metrics.measureDurationUnscoped(this.scope, duration, maxDuration);
['Apdex/' + partialName, 'Apdex'].forEach(function (name) {
var apdexStats = metrics.getOrCreateApdexMetric(name).stats;
if (isError) {
apdexStats.incrementFrustrating();
}
else {
apdexStats.recordValueInMillis(duration);
}
});
['Apdex/' + partialName, 'Apdex'].forEach(function (name) {
var apdexStats = metrics.getOrCreateApdexMetric(name).stats;
if (isError) {
apdexStats.incrementFrustrating();
}
else {
apdexStats.recordValueInMillis(duration);
}
});
}

@@ -161,0 +169,0 @@ return this.scope;

@@ -69,3 +69,3 @@ 'use strict';

TraceAggregator.prototype.add = function add(transaction) {
if (transaction && transaction.metrics) {
if (transaction && transaction.metrics && !transaction.ignore) {
var trace = transaction.getTrace()

@@ -72,0 +72,0 @@ , scope = transaction.scope

@@ -71,3 +71,11 @@ 'use strict';

Object.keys(parsed.query).forEach(function (key) {
if (parsed.query[key] === '') {
/* 'var1&var2=value' is not necessarily the same as 'var1=&var2=value'
*
* In my world, one is an assertion of presence, and the other is
* an empty variable. Some web frameworks behave this way as well,
* so don't lose information.
*
* TODO: figure out if this confuses everyone and remove if so.
*/
if (parsed.query[key] === '' && parsed.path.indexOf(key + '=') < 0) {
segment.parameters[key] = true;

@@ -74,0 +82,0 @@ }

@@ -0,1 +1,11 @@

### v0.9.13-101 / beta-13 (2013-01-07):
* When New Relic's servers (or an intermediate proxy) returned a response with
a status code other than 20x, the entire instrumented application would
crash.
* Some metric normalization rules were not being interpreted correctly, leading
to malformed normalized metric names.
* Metric normalization rules that specifed that matching metrics were to be
ignored were not being enforced.
### v0.9.12-91 / beta-12 (2012-12-28):

@@ -2,0 +12,0 @@

{
"name": "newrelic",
"version": "0.9.12-92",
"version": "0.9.13-101",
"author": "New Relic Node.js agent team <nodejs@newrelic.com>",

@@ -5,0 +5,0 @@ "contributors": [

@@ -11,5 +11,5 @@ # New Relic Node.js agent

1. [Install node](http://nodejs.org/#download). For now, at least 0.6 is
required. Some features (e.g. error tracing) depend in whole or in part on
features in 0.8 and above. Development work is being done against the latest
released version.
required. Some features (e.g. error tracing) depend in whole or in
part on features in 0.8 and above. Development work on the agentis
being done against the latest released non-development version of Node.
2. Install this module via `npm install newrelic` for the application you

@@ -29,18 +29,19 @@ want to monitor.

When you start your app, the agent should start up with it and start reporting
data that will appear within our UI after a few minutes. Because the agent
minimizes the amount of bandwidth it consumes, it only reports metrics, errors
and transaction traces once a minute, so if you add the agent to tests that run
in under a minute, the agent won't have time to report data to New Relic. The
agent will write its log to a file named `newrelic_agent.log` in the
application directory. If the agent doesn't send data or crashes your app, the
log can help New Relic determine what went wrong, so be sure to send it along
with any bug reports or support requests.
When you start your app, the agent should start up with it and start
reporting data that will appear within our UI after a few minutes.
Because the agent minimizes the amount of bandwidth it consumes, it
only reports data once a minute, so if you add the agent to tests
that take less than a minute to run, the agent won't have time to
report data to New Relic. The agent will write its log to a file named
`newrelic_agent.log` in the application directory. If the agent doesn't
send data or crashes your app, the log can help New Relic determine what
went wrong, so be sure to send it along with any bug reports or support
requests.
## Configuring the agent
The agent can be tailored to your app's requirements, both from the server and
via the newrelic.js configuration file you created above. For more details on
what can be configured, refer to `lib/config.default.js`, which documents
the available variables and their default values.
The agent can be tailored to your app's requirements, both from the
server and via the newrelic.js configuration file you created. For more
details on what can be configured, refer to `lib/config.default.js`,
which documents the available variables and their default values.

@@ -47,0 +48,0 @@ In addition, for those of you running in Heroku, Microsoft Azure or any other

@@ -65,7 +65,6 @@ 'use strict';

var sender = new DataSender(agent.config, SAMPLE_RUN_ID);
// stub out DataSender.send
sender.send = function (sMethod, sUri, sParams) {
method = sMethod;
uri = sUri;
params = sParams;
sender.invokeMethod = function (sMethod, sParams) {
method = sMethod;
uri = sender.getURL(method);
params = sParams;
};

@@ -72,0 +71,0 @@

'use strict';
var path = require('path')
, chai = require('chai')
, expect = chai.expect
, sinon = require('sinon')
, DataSender = require(path.join(__dirname, '..', 'lib', 'collector', 'data-sender'))
var path = require('path')
, chai = require('chai')
, expect = chai.expect
, should = chai.should()
, EventEmitter = require('events').EventEmitter
, Stream = require('stream')
, DataSender = require(path.join(__dirname, '..', 'lib', 'collector', 'data-sender'))
;

@@ -28,7 +30,15 @@

var raw = '/listener/invoke';
var expected = 'http://collector.newrelic.com:80/listener/invoke';
expect(sender.canonicalizeURL(raw)).equal(expected);
var expected = 'http://collector.newrelic.com:80' +
'/agent_listener/invoke_raw_method' +
'?marshal_format=json&protocol_version=9&' +
'license_key=&method=test&agent_run_id=12';
expect(sender.getURL('test')).equal(expected);
});
it("should require a message type when invoking a remote method", function () {
var sender = new DataSender();
expect(function () { sender.invokeMethod(); })
.throws("Can't send the collector a message without a message type");
});
describe("when generating headers for a plain request", function () {

@@ -44,3 +54,3 @@ var headers;

headers = sender.createHeaders(80);
headers = sender.getHeaders('test');
});

@@ -53,3 +63,3 @@

it("should use the content length from the parameter", function () {
expect(headers["Content-Length"]).equal(80);
expect(headers["Content-Length"]).equal(4);
});

@@ -84,3 +94,3 @@

headers = sender.createHeaders(92, true);
headers = sender.getHeaders('zxxvxzxzzx', true);
});

@@ -93,3 +103,3 @@

it("should use the content length from the parameter", function () {
expect(headers["Content-Length"]).equal(92);
expect(headers["Content-Length"]).equal(10);
});

@@ -114,5 +124,4 @@

describe("when performing RPC against the collector", function () {
describe("when generating the collector URL", function () {
var sender
, mockSender
, TEST_RUN_ID = Math.floor(Math.random() * 3000)

@@ -128,29 +137,99 @@ ;

sender = new DataSender(config, TEST_RUN_ID);
mockSender = sinon.mock(sender);
});
it("should always add the agent run ID, if set", function () {
var METHOD = 'TEST'
, PARAMS = {test : "value"}
, URL = sinon.match(new RegExp('agent_run_id=' + TEST_RUN_ID))
;
expect(sender.agentRunId).equal(TEST_RUN_ID);
expect(sender.getURL('TEST_METHOD')).match(new RegExp('agent_run_id=' + TEST_RUN_ID));
});
mockSender.expects('send').once().withExactArgs(METHOD, URL, PARAMS);
sender.invokeMethod(METHOD, PARAMS);
it("should correctly set up the method", function () {
expect(sender.getURL('TEST_METHOD')).match(/method=TEST_METHOD/);
});
});
mockSender.verify();
describe("when processing a collector response", function () {
var sender;
beforeEach(function () {
sender = new DataSender({host : 'localhost'});
});
it("should correctly set up the method", function () {
var METHOD = 'TEST'
, PARAMS = {test : "value"}
, URL = sinon.match(new RegExp('method=' + METHOD))
;
it("should raise an error if the response has an error status code", function (done) {
var response = new EventEmitter();
response.statusCode = 401;
mockSender.expects('send').once().withExactArgs(METHOD, URL, PARAMS);
sender.invokeMethod(METHOD, PARAMS);
sender.on('error', function (message, error) {
expect(error.message).equal("Got HTTP 401 in response to TEST.");
mockSender.verify();
return done();
});
sender.onCollectorResponse('TEST', response);
});
it("should hand off the response to the message handler", function (done) {
var response = new Stream();
response.setEncoding = function () {}; // fake it til you make it
response.readable = true;
response.statusCode = 200;
var sampleBody = '{"return_value":{"messages":[]}}';
sender.handleMessage = function (message, error, body) {
expect(message).equal('TEST');
should.not.exist(error);
expect(body).equal(sampleBody);
return done();
};
sender.onCollectorResponse('TEST', response);
process.nextTick(function () {
response.emit('data', sampleBody);
response.emit('end');
});
});
});
describe("when handling a response's message", function () {
var sender;
beforeEach(function () {
sender = new DataSender({host : 'localhost'});
});
it("should hand off decoding errors", function (done) {
sender.on('error', function (message, error) {
expect(message).equal('TEST');
expect(error.message).equal('unspecified decoding error');
return done();
});
sender.handleMessage('TEST', new Error('unspecified decoding error'));
});
it("should hand off server exceptions", function (done) {
sender.on('error', function (message, error) {
expect(message).equal('TEST');
expect(error).eql({error_type : 'NewRelic::Agent::ForceRestartException'});
return done();
});
var body = '{"exception":{"error_type":"NewRelic::Agent::ForceRestartException"}}';
sender.handleMessage('TEST', null, body);
});
it("should hand off return_value, if set", function (done) {
sender.on('response', function (response) {
expect(response).eql({url_rules : []});
return done();
});
var body = '{"return_value":{"url_rules":[]}}';
sender.handleMessage('TEST', null, body);
});
});
});

@@ -70,2 +70,43 @@ 'use strict';

describe("with Saxon's patterns", function () {
describe("including '^(?!account|application).*'", function () {
beforeEach(function () {
rule = new Rule({
"each_segment" : true,
"match_expression" : "^(?!account|application).*",
"replacement" : "*"
});
});
it("implies '/account/myacc/application/test' -> '/account/*/application/*'",
function () {
expect(rule.apply('/account/myacc/application/test'))
.equal('/account/*/application/*');
});
it("implies '/oh/dude/account/myacc/application' -> '/*/*/account/*/application'",
function () {
expect(rule.apply('/oh/dude/account/myacc/application'))
.equal('/*/*/account/*/application');
});
});
describe("including '^(?!channel|download|popups|search|tap|user|related|admin|api|genres|notification).*'",
function () {
beforeEach(function () {
rule = new Rule({
"each_segment" : true,
"match_expression" : "^(?!channel|download|popups|search|tap|user|related|admin|api|genres|notification).*",
"replacement" : "*"
});
});
it("implies '/tap/stuff/user/gfy77t/view' -> '/tap/*/user/*/*'",
function () {
expect(rule.apply('/tap/stuff/user/gfy77t/view'))
.equal('/tap/*/user/*/*');
});
});
});
describe("with a more complex substitution rule", function () {

@@ -144,33 +185,33 @@ before(function () {

it("should default to not applying the rule to each segment", function () {
expect((new Rule()).eachSegment).equal(false);
expect(new Rule().eachSegment).equal(false);
});
it("should default the rule's precedence to 0", function () {
expect((new Rule()).precedence).equal(0);
expect(new Rule().precedence).equal(0);
});
it("should default to not terminating rule evaluation", function () {
expect((new Rule()).isTerminal).equal(false);
expect(new Rule().isTerminal).equal(false);
});
it("should have a regexp that matches the empty string", function () {
expect((new Rule()).patternString).equal('^$');
expect(new Rule().pattern).eql(/^$/);
});
it("should use the entire match as the replacement value", function () {
expect((new Rule()).replacement).equal('$0');
expect(new Rule().replacement).equal('$0');
});
it("should default to not replacing all instances", function () {
expect((new Rule()).replaceAll).equal(false);
expect(new Rule().replaceAll).equal(false);
});
it("should default to not ignoring matching URLs", function () {
expect((new Rule()).ignore).equal(false);
expect(new Rule().ignore).equal(false);
});
it("should silently pass through the input if applied", function () {
expect((new Rule()).apply('sample/input')).equal('sample/input');
expect(new Rule().apply('sample/input')).equal('sample/input');
});
});
});
'use strict';
var path = require('path')
, chai = require('chai')
, expect = chai.expect
var path = require('path')
, chai = require('chai')
, expect = chai.expect
, Normalizer = require(path.join(__dirname, '..', 'lib', 'metrics', 'normalizer'))

@@ -10,72 +10,164 @@ ;

describe ("MetricNormalizer", function () {
// captured from staging-collector-1.newrelic.com on 2012-08-29
var rules =
[{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '^(test_match_nothing)$',
replace_all : false, ignore : false, replacement : '\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$',
replace_all : false, ignore : false, replacement : '/*.\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '^(test_match_nothing)$',
replace_all : false, ignore : false, replacement : '\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '^(test_match_nothing)$',
replace_all : false, ignore : false, replacement : '\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$',
replace_all : false, ignore : false, replacement : '/*.\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$',
replace_all : false, ignore : false, replacement : '/*.\\1'},
{each_segment : true, eval_order : 1, terminate_chain : false,
match_expression : '^[0-9][0-9a-f_,.-]*$',
replace_all : false, ignore : false, replacement : '*'},
{each_segment : true, eval_order : 1, terminate_chain : false,
match_expression : '^[0-9][0-9a-f_,.-]*$',
replace_all : false, ignore : false, replacement : '*'},
{each_segment : true, eval_order : 1, terminate_chain : false,
match_expression : '^[0-9][0-9a-f_,.-]*$',
replace_all : false, ignore : false, replacement : '*'},
{each_segment : false, eval_order : 2, terminate_chain : false,
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$',
replace_all : false, ignore : false, replacement : '\\1/.*\\2'},
{each_segment : false, eval_order : 2, terminate_chain : false,
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$',
replace_all : false, ignore : false, replacement : '\\1/.*\\2'},
{each_segment : false, eval_order : 2, terminate_chain : false,
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$',
replace_all : false, ignore : false, replacement : '\\1/.*\\2'}];
it("shouldn't throw when instantiated without any rules", function () {
expect(function () { var normalizer = new Normalizer(); }).not.throws();
});
var samples = {url_rules : rules};
it("should normalize even without any rules set", function () {
expect(function () {
expect(new Normalizer().normalize('/sample')).eql({name : '/sample'});
}).not.throws();
});
it("shouldn't throw when instantiated without any rules", function () {
expect(function () { var normalizer = new Normalizer(); });
it("should normalize with an empty rule set", function () {
expect(function () {
var normalizer = new Normalizer();
normalizer.load({url_rules : []});
expect(normalizer.normalize('/sample')).eql({name : '/sample'});
}).not.throws();
});
it("shouldn't throw when instantiated with a full set of rules", function () {
expect(function () { var normalizer = new Normalizer(samples); });
describe("with rules captured from the staging collector on 2012-08-29",
function () {
var sample = {
url_rules : [
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '^(test_match_nothing)$',
replace_all : false, ignore : false, replacement : '\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$',
replace_all : false, ignore : false, replacement : '/*.\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '^(test_match_nothing)$',
replace_all : false, ignore : false, replacement : '\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '^(test_match_nothing)$',
replace_all : false, ignore : false, replacement : '\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$',
replace_all : false, ignore : false, replacement : '/*.\\1'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '.*\\.(css|gif|ico|jpe?g|js|png|swf)$',
replace_all : false, ignore : false, replacement : '/*.\\1'},
{each_segment : true, eval_order : 1, terminate_chain : false,
match_expression : '^[0-9][0-9a-f_,.-]*$',
replace_all : false, ignore : false, replacement : '*'},
{each_segment : true, eval_order : 1, terminate_chain : false,
match_expression : '^[0-9][0-9a-f_,.-]*$',
replace_all : false, ignore : false, replacement : '*'},
{each_segment : true, eval_order : 1, terminate_chain : false,
match_expression : '^[0-9][0-9a-f_,.-]*$',
replace_all : false, ignore : false, replacement : '*'},
{each_segment : false, eval_order : 2, terminate_chain : false,
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$',
replace_all : false, ignore : false, replacement : '\\1/.*\\2'},
{each_segment : false, eval_order : 2, terminate_chain : false,
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$',
replace_all : false, ignore : false, replacement : '\\1/.*\\2'},
{each_segment : false, eval_order : 2, terminate_chain : false,
match_expression : '^(.*)/[0-9][0-9a-f_,-]*\\.([0-9a-z][0-9a-z]*)$',
replace_all : false, ignore : false, replacement : '\\1/.*\\2'}
]
};
it("shouldn't throw when instantiated with a full set of rules", function () {
expect(function () { var normalizer = new Normalizer(sample); }).not.throws();
});
it("should eliminate duplicate rules as part of loading them", function () {
var reduced = [
{eachSegment : false, precedence : 0, isTerminal : true,
pattern: /^(test_match_nothing)$/,
replaceAll : false, ignore : false, replacement : '$1'},
{eachSegment : false, precedence : 0, isTerminal : true,
pattern: /.*\\.(css|gif|ico|jpe?g|js|png|swf)$/,
replaceAll : false, ignore : false, replacement : '/*.$1'},
{eachSegment : true, precedence : 1, isTerminal : false,
pattern: /^[0-9][0-9a-f_,.\-]*$/,
replaceAll : false, ignore : false, replacement : '*'},
{eachSegment : false, precedence : 2, isTerminal : false,
pattern: /^(.*)\/[0-9][0-9a-f_,\-]*\\.([0-9a-z][0-9a-z]*)$/,
replaceAll : false, ignore : false, replacement : '$1/.*$2'}
];
var normalizer = new Normalizer(sample);
expect(normalizer.rules).eql(reduced);
});
it("should normalize a JPEGgy URL", function () {
expect(new Normalizer(sample).normalize('/excessivity.jpeg')).eql({
name : '/excessivity.jpeg',
normalized : '/*.jpeg',
terminal : true
});
});
it("should normalize a JPGgy URL", function () {
expect(new Normalizer(sample).normalize('/excessivity.jpg')).eql({
name : '/excessivity.jpg',
normalized : '/*.jpg',
terminal : true
});
});
it("should normalize a CSS URL", function () {
expect(new Normalizer(sample).normalize('/style.css')).eql({
name : '/style.css',
normalized : '/*.css',
terminal : true
});
});
});
it("should normalize a JPEGgy URL", function () {
var normalizer = new Normalizer(samples);
var normalized = normalizer.normalizeUrl('/excessivity.jpeg');
expect(normalized).equal('/*.jpeg');
it("should correctly ignore a matching name", function () {
var sample = {
url_rules : [
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '^/long_polling$',
replace_all : false, ignore : true, replacement : '*'}
]
};
expect(new Normalizer(sample).normalize('/long_polling')).eql({
name : '/long_polling',
ignore : true
});
});
it("should normalize a JPGgy URL", function () {
var normalizer = new Normalizer(samples);
var normalized = normalizer.normalizeUrl('/excessivity.jpg');
expect(normalized).equal('/*.jpg');
it("should apply rules by precedence", function () {
var sample = {
url_rules : [
{each_segment : true, eval_order : 1, terminate_chain : false,
match_expression : 'mochi',
replace_all : false, ignore : false, replacement : 'millet'},
{each_segment : false, eval_order : 0, terminate_chain : false,
match_expression : '/rice$',
replace_all : false, ignore : false, replacement : '/mochi'}
]
};
expect(new Normalizer(sample).normalize('/rice/is/not/rice')).eql({
name : '/rice/is/not/rice',
normalized : '/rice/is/not/millet'
});
});
it("should normalize a CSS URL", function () {
var normalizer = new Normalizer(samples);
var normalized = normalizer.normalizeUrl('/style.css');
expect(normalized).equal('/*.css');
it("should terminate when indicated by rule", function () {
var sample = {
url_rules : [
{each_segment : true, eval_order : 1, terminate_chain : false,
match_expression : 'mochi',
replace_all : false, ignore : false, replacement : 'millet'},
{each_segment : false, eval_order : 0, terminate_chain : true,
match_expression : '/rice$',
replace_all : false, ignore : false, replacement : '/mochi'}
]
};
expect(new Normalizer(sample).normalize('/rice/is/not/rice')).eql({
name : '/rice/is/not/rice',
normalized : '/rice/is/not/mochi',
terminal : true
});
});
it("should apply rules by precedence");
it("should terminate when indicated by rule");
it("should use precedence to decide how to apply multiple rules");
});

@@ -66,3 +66,3 @@ 'use strict';

var segment = new TraceSegment(new Trace('Test/TraceExample03'), 'UnitTest');
webChild = segment.addWeb('/test?test1=value1&test2&test3=50');
webChild = segment.addWeb('/test?test1=value1&test2&test3=50&test4=');

@@ -86,6 +86,10 @@ trace.setDurationInMillis(1, 0);

it("should set parameters with empty values to true", function () {
it("should set bare parameters to true (as in present)", function () {
expect(webChild.parameters.test2).equal(true);
});
it("should set parameters with empty values to ''", function () {
expect(webChild.parameters.test4).equal('');
});
it("should serialize the segment with the parameters", function () {

@@ -96,3 +100,3 @@ var expected = [

'WebTransaction/Uri/test',
{test1 : 'value1', test2 : true, test3 : '50'},
{test1 : 'value1', test2 : true, test3 : '50', test4 : ''},
[]

@@ -99,0 +103,0 @@ ];

### KNOWN ISSUES:
* The metric names displayed in New Relic are a work in progress. The
flexibility of Node's HTTP handling and routing presents unique
challenges to the New Relic data model. We're working on a set of
strategies to improve how metrics are named, but be aware that metric
names may change over time as these strategies are implemented.
* There are irregularities around transaction trace capture and display.
If you notice missing or incorrect information from transaction traces,
let us know.
* There are over 20,000 modules on npm. We can only instrument a tiny
number of them. Even for the modules we support, there are a very
large number of ways to use them. If you see data you don't expect on
New Relic and have the time to produce a reduced version of the code
that is producing the strange data, it will be gratefully used to
improve the agent.
* There is an error tracer in the Node agent, but it's a work in progress.
In particular, it still does not intercept errors that may already be
handled by frameworks. Also, parts of it depend on the new, experimental
[domain](http://nodejs.org/api/domain.html) API added in Node 0.8, and
domain-specific functionality will not work in apps running in
Node 0.6.x.
* The CPU and memory overhead incurred by the Node agent is relatively

@@ -11,6 +31,2 @@ minor (~1-10%, depending on how much of the instrumentation your

applications.
* There are irregularities around transaction trace capture and display.
If you notice missing or incorrect information from transaction traces,
let us know. If possible, include the package.json for your application
with your report.
* The agent works only with Node.js 0.6 and newer.

@@ -26,2 +42,3 @@ * When using Node's included clustering support, each worker process will

2. CouchDB (not pre-GA)
* Log rotation for the agent log.
* Better tests for existing instrumentation.

@@ -28,0 +45,0 @@ * Differentiate between HTTP and HTTPS connections.

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc