UNPKG

10.8 kBJavaScriptView Raw
1'use strict';
2
3var fs = require('fs');
4var url = require('url');
5var transports = require('./transports');
6var path = require('path');
7var lsmod = require('../vendor/node-lsmod');
8var stacktrace = require('stack-trace');
9var stringify = require('../vendor/json-stringify-safe');
10
11var ravenVersion = require('../package.json').version;
12
13var protocolMap = {
14 http: 80,
15 https: 443
16};
17
18var consoleAlerts = new Set();
19
20// Default Node.js REPL depth
21var MAX_SERIALIZE_EXCEPTION_DEPTH = 3;
22// 50kB, as 100kB is max payload size, so half sounds reasonable
23var MAX_SERIALIZE_EXCEPTION_SIZE = 50 * 1024;
24var MAX_SERIALIZE_KEYS_LENGTH = 40;
25
26function utf8Length(value) {
27 return ~-encodeURI(value).split(/%..|./).length;
28}
29
30function jsonSize(value) {
31 return utf8Length(JSON.stringify(value));
32}
33
34function isError(what) {
35 return (
36 Object.prototype.toString.call(what) === '[object Error]' || what instanceof Error
37 );
38}
39
40module.exports.isError = isError;
41
42function isPlainObject(what) {
43 return Object.prototype.toString.call(what) === '[object Object]';
44}
45
46module.exports.isPlainObject = isPlainObject;
47
48function serializeValue(value) {
49 var maxLength = 40;
50
51 if (typeof value === 'string') {
52 return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026';
53 } else if (
54 typeof value === 'number' ||
55 typeof value === 'boolean' ||
56 typeof value === 'undefined'
57 ) {
58 return value;
59 }
60
61 var type = Object.prototype.toString.call(value);
62
63 // Node.js REPL notation
64 if (type === '[object Object]') return '[Object]';
65 if (type === '[object Array]') return '[Array]';
66 if (type === '[object Function]')
67 return value.name ? '[Function: ' + value.name + ']' : '[Function]';
68
69 return value;
70}
71
72function serializeObject(value, depth) {
73 if (depth === 0) return serializeValue(value);
74
75 if (isPlainObject(value)) {
76 return Object.keys(value).reduce(function(acc, key) {
77 acc[key] = serializeObject(value[key], depth - 1);
78 return acc;
79 }, {});
80 } else if (Array.isArray(value)) {
81 return value.map(function(val) {
82 return serializeObject(val, depth - 1);
83 });
84 }
85
86 return serializeValue(value);
87}
88
89function serializeException(ex, depth, maxSize) {
90 if (!isPlainObject(ex)) return ex;
91
92 depth = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_DEPTH : depth;
93 maxSize = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_SIZE : maxSize;
94
95 var serialized = serializeObject(ex, depth);
96
97 if (jsonSize(stringify(serialized)) > maxSize) {
98 return serializeException(ex, depth - 1);
99 }
100
101 return serialized;
102}
103
104module.exports.serializeException = serializeException;
105
106function serializeKeysForMessage(keys, maxLength) {
107 if (typeof keys === 'number' || typeof keys === 'string') return keys.toString();
108 if (!Array.isArray(keys)) return '';
109
110 keys = keys.filter(function(key) {
111 return typeof key === 'string';
112 });
113 if (keys.length === 0) return '[object has no keys]';
114
115 maxLength = typeof maxLength !== 'number' ? MAX_SERIALIZE_KEYS_LENGTH : maxLength;
116 if (keys[0].length >= maxLength) return keys[0];
117
118 for (var usedKeys = keys.length; usedKeys > 0; usedKeys--) {
119 var serialized = keys.slice(0, usedKeys).join(', ');
120 if (serialized.length > maxLength) continue;
121 if (usedKeys === keys.length) return serialized;
122 return serialized + '\u2026';
123 }
124
125 return '';
126}
127
128module.exports.serializeKeysForMessage = serializeKeysForMessage;
129
130module.exports.disableConsoleAlerts = function disableConsoleAlerts() {
131 consoleAlerts = false;
132};
133
134module.exports.enableConsoleAlerts = function enableConsoleAlerts() {
135 consoleAlerts = new Set();
136};
137
138module.exports.consoleAlert = function consoleAlert(msg) {
139 if (consoleAlerts) {
140 console.warn('raven@' + ravenVersion + ' alert: ' + msg);
141 }
142};
143
144module.exports.consoleAlertOnce = function consoleAlertOnce(msg) {
145 if (consoleAlerts && !consoleAlerts.has(msg)) {
146 consoleAlerts.add(msg);
147 console.warn('raven@' + ravenVersion + ' alert: ' + msg);
148 }
149};
150
151module.exports.extend =
152 Object.assign ||
153 function(target) {
154 for (var i = 1; i < arguments.length; i++) {
155 var source = arguments[i];
156 for (var key in source) {
157 if (Object.prototype.hasOwnProperty.call(source, key)) {
158 target[key] = source[key];
159 }
160 }
161 }
162 return target;
163 };
164
165module.exports.getAuthHeader = function getAuthHeader(timestamp, apiKey, apiSecret) {
166 var header = ['Sentry sentry_version=5'];
167 header.push('sentry_timestamp=' + timestamp);
168 header.push('sentry_client=raven-node/' + ravenVersion);
169 header.push('sentry_key=' + apiKey);
170 if (apiSecret) header.push('sentry_secret=' + apiSecret);
171 return header.join(', ');
172};
173
174module.exports.parseDSN = function parseDSN(dsn) {
175 if (!dsn) {
176 // Let a falsey value return false explicitly
177 return false;
178 }
179 try {
180 var parsed = url.parse(dsn),
181 response = {
182 protocol: parsed.protocol.slice(0, -1),
183 public_key: parsed.auth.split(':')[0],
184 host: parsed.host.split(':')[0]
185 };
186
187 if (parsed.auth.split(':')[1]) {
188 response.private_key = parsed.auth.split(':')[1];
189 }
190
191 if (~response.protocol.indexOf('+')) {
192 response.protocol = response.protocol.split('+')[1];
193 }
194
195 if (!transports.hasOwnProperty(response.protocol)) {
196 throw new Error('Invalid transport');
197 }
198
199 var index = parsed.pathname.lastIndexOf('/');
200 response.path = parsed.pathname.substr(0, index + 1);
201 response.project_id = parsed.pathname.substr(index + 1);
202 response.port = ~~parsed.port || protocolMap[response.protocol] || 443;
203 return response;
204 } catch (e) {
205 throw new Error('Invalid Sentry DSN: ' + dsn);
206 }
207};
208
209module.exports.getTransaction = function getTransaction(frame) {
210 if (frame.module || frame.function) {
211 return (frame.module || '?') + ' at ' + (frame.function || '?');
212 }
213 return '<unknown>';
214};
215
216var moduleCache;
217module.exports.getModules = function getModules() {
218 if (!moduleCache) {
219 moduleCache = lsmod();
220 }
221 return moduleCache;
222};
223
224module.exports.fill = function(obj, name, replacement, track) {
225 var orig = obj[name];
226 obj[name] = replacement(orig);
227 if (track) {
228 track.push([obj, name, orig]);
229 }
230};
231
232var LINES_OF_CONTEXT = 7;
233
234function getFunction(line) {
235 try {
236 return (
237 line.getFunctionName() ||
238 line.getTypeName() + '.' + (line.getMethodName() || '<anonymous>')
239 );
240 } catch (e) {
241 // This seems to happen sometimes when using 'use strict',
242 // stemming from `getTypeName`.
243 // [TypeError: Cannot read property 'constructor' of undefined]
244 return '<anonymous>';
245 }
246}
247
248var mainModule =
249 ((require.main && require.main.filename && path.dirname(require.main.filename)) ||
250 global.process.cwd()) + '/';
251
252function getModule(filename, base) {
253 if (!base) base = mainModule;
254
255 // It's specifically a module
256 var file = path.basename(filename, '.js');
257 filename = path.dirname(filename);
258 var n = filename.lastIndexOf('/node_modules/');
259 if (n > -1) {
260 // /node_modules/ is 14 chars
261 return filename.substr(n + 14).replace(/\//g, '.') + ':' + file;
262 }
263 // Let's see if it's a part of the main module
264 // To be a part of main module, it has to share the same base
265 n = (filename + '/').lastIndexOf(base, 0);
266 if (n === 0) {
267 var module = filename.substr(base.length).replace(/\//g, '.');
268 if (module) module += ':';
269 module += file;
270 return module;
271 }
272 return file;
273}
274
275function readSourceFiles(filenames, cb) {
276 // we're relying on filenames being de-duped already
277 if (filenames.length === 0) return setTimeout(cb, 0, {});
278
279 var sourceFiles = {};
280 var numFilesToRead = filenames.length;
281 return filenames.forEach(function(filename) {
282 fs.readFile(filename, function(readErr, file) {
283 if (!readErr) sourceFiles[filename] = file.toString().split('\n');
284 if (--numFilesToRead === 0) cb(sourceFiles);
285 });
286 });
287}
288
289// This is basically just `trim_line` from https://github.com/getsentry/sentry/blob/master/src/sentry/lang/javascript/processor.py#L67
290function snipLine(line, colno) {
291 var ll = line.length;
292 if (ll <= 150) return line;
293 if (colno > ll) colno = ll;
294
295 var start = Math.max(colno - 60, 0);
296 if (start < 5) start = 0;
297
298 var end = Math.min(start + 140, ll);
299 if (end > ll - 5) end = ll;
300 if (end === ll) start = Math.max(end - 140, 0);
301
302 line = line.slice(start, end);
303 if (start > 0) line = '{snip} ' + line;
304 if (end < ll) line += ' {snip}';
305
306 return line;
307}
308
309function snipLine0(line) {
310 return snipLine(line, 0);
311}
312
313function parseStack(err, cb) {
314 if (!err) return cb([]);
315
316 var stack = stacktrace.parse(err);
317 if (!stack || !Array.isArray(stack) || !stack.length || !stack[0].getFileName) {
318 // the stack is not the useful thing we were expecting :/
319 return cb([]);
320 }
321
322 // Sentry expects the stack trace to be oldest -> newest, v8 provides newest -> oldest
323 stack.reverse();
324
325 var frames = [];
326 var filesToRead = {};
327 stack.forEach(function(line) {
328 var frame = {
329 filename: line.getFileName() || '',
330 lineno: line.getLineNumber(),
331 colno: line.getColumnNumber(),
332 function: getFunction(line)
333 };
334
335 var isInternal =
336 line.isNative() ||
337 (frame.filename[0] !== '/' &&
338 frame.filename[0] !== '.' &&
339 frame.filename.indexOf(':\\') !== 1);
340
341 // in_app is all that's not an internal Node function or a module within node_modules
342 // note that isNative appears to return true even for node core libraries
343 // see https://github.com/getsentry/raven-node/issues/176
344 frame.in_app = !isInternal && frame.filename.indexOf('node_modules/') === -1;
345
346 // Extract a module name based on the filename
347 if (frame.filename) {
348 frame.module = getModule(frame.filename);
349 if (!isInternal) filesToRead[frame.filename] = true;
350 }
351
352 frames.push(frame);
353 });
354
355 return readSourceFiles(Object.keys(filesToRead), function(sourceFiles) {
356 frames.forEach(function(frame) {
357 if (frame.filename && sourceFiles[frame.filename]) {
358 var lines = sourceFiles[frame.filename];
359 try {
360 frame.pre_context = lines
361 .slice(Math.max(0, frame.lineno - (LINES_OF_CONTEXT + 1)), frame.lineno - 1)
362 .map(snipLine0);
363 frame.context_line = snipLine(lines[frame.lineno - 1], frame.colno);
364 frame.post_context = lines
365 .slice(frame.lineno, frame.lineno + LINES_OF_CONTEXT)
366 .map(snipLine0);
367 } catch (e) {
368 // anomaly, being defensive in case
369 // unlikely to ever happen in practice but can definitely happen in theory
370 }
371 }
372 });
373
374 cb(frames);
375 });
376}
377
378// expose basically for testing because I don't know what I'm doing
379module.exports.parseStack = parseStack;
380module.exports.getModule = getModule;