1 | 'use strict';
|
2 |
|
3 | var fs = require('fs');
|
4 | var url = require('url');
|
5 | var transports = require('./transports');
|
6 | var path = require('path');
|
7 | var lsmod = require('../vendor/node-lsmod');
|
8 | var stacktrace = require('stack-trace');
|
9 | var stringify = require('../vendor/json-stringify-safe');
|
10 |
|
11 | var ravenVersion = require('../package.json').version;
|
12 |
|
13 | var protocolMap = {
|
14 | http: 80,
|
15 | https: 443
|
16 | };
|
17 |
|
18 | var consoleAlerts = new Set();
|
19 |
|
20 |
|
21 | var MAX_SERIALIZE_EXCEPTION_DEPTH = 3;
|
22 |
|
23 | var MAX_SERIALIZE_EXCEPTION_SIZE = 50 * 1024;
|
24 | var MAX_SERIALIZE_KEYS_LENGTH = 40;
|
25 |
|
26 | function utf8Length(value) {
|
27 | return ~-encodeURI(value).split(/%..|./).length;
|
28 | }
|
29 |
|
30 | function jsonSize(value) {
|
31 | return utf8Length(JSON.stringify(value));
|
32 | }
|
33 |
|
34 | function isError(what) {
|
35 | return (
|
36 | Object.prototype.toString.call(what) === '[object Error]' || what instanceof Error
|
37 | );
|
38 | }
|
39 |
|
40 | module.exports.isError = isError;
|
41 |
|
42 | function isPlainObject(what) {
|
43 | return Object.prototype.toString.call(what) === '[object Object]';
|
44 | }
|
45 |
|
46 | module.exports.isPlainObject = isPlainObject;
|
47 |
|
48 | function 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 |
|
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 |
|
72 | function 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 |
|
89 | function 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 |
|
104 | module.exports.serializeException = serializeException;
|
105 |
|
106 | function 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 |
|
128 | module.exports.serializeKeysForMessage = serializeKeysForMessage;
|
129 |
|
130 | module.exports.disableConsoleAlerts = function disableConsoleAlerts() {
|
131 | consoleAlerts = false;
|
132 | };
|
133 |
|
134 | module.exports.enableConsoleAlerts = function enableConsoleAlerts() {
|
135 | consoleAlerts = new Set();
|
136 | };
|
137 |
|
138 | module.exports.consoleAlert = function consoleAlert(msg) {
|
139 | if (consoleAlerts) {
|
140 | console.warn('raven@' + ravenVersion + ' alert: ' + msg);
|
141 | }
|
142 | };
|
143 |
|
144 | module.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 |
|
151 | module.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 |
|
165 | module.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 |
|
174 | module.exports.parseDSN = function parseDSN(dsn) {
|
175 | if (!dsn) {
|
176 |
|
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 |
|
209 | module.exports.getTransaction = function getTransaction(frame) {
|
210 | if (frame.module || frame.function) {
|
211 | return (frame.module || '?') + ' at ' + (frame.function || '?');
|
212 | }
|
213 | return '<unknown>';
|
214 | };
|
215 |
|
216 | var moduleCache;
|
217 | module.exports.getModules = function getModules() {
|
218 | if (!moduleCache) {
|
219 | moduleCache = lsmod();
|
220 | }
|
221 | return moduleCache;
|
222 | };
|
223 |
|
224 | module.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 |
|
232 | var LINES_OF_CONTEXT = 7;
|
233 |
|
234 | function getFunction(line) {
|
235 | try {
|
236 | return (
|
237 | line.getFunctionName() ||
|
238 | line.getTypeName() + '.' + (line.getMethodName() || '<anonymous>')
|
239 | );
|
240 | } catch (e) {
|
241 |
|
242 |
|
243 |
|
244 | return '<anonymous>';
|
245 | }
|
246 | }
|
247 |
|
248 | var mainModule =
|
249 | ((require.main && require.main.filename && path.dirname(require.main.filename)) ||
|
250 | global.process.cwd()) + '/';
|
251 |
|
252 | function getModule(filename, base) {
|
253 | if (!base) base = mainModule;
|
254 |
|
255 |
|
256 | var file = path.basename(filename, '.js');
|
257 | filename = path.dirname(filename);
|
258 | var n = filename.lastIndexOf('/node_modules/');
|
259 | if (n > -1) {
|
260 |
|
261 | return filename.substr(n + 14).replace(/\//g, '.') + ':' + file;
|
262 | }
|
263 |
|
264 |
|
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 |
|
275 | function readSourceFiles(filenames, cb) {
|
276 |
|
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 |
|
290 | function 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 |
|
309 | function snipLine0(line) {
|
310 | return snipLine(line, 0);
|
311 | }
|
312 |
|
313 | function 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 |
|
319 | return cb([]);
|
320 | }
|
321 |
|
322 |
|
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 |
|
369 |
|
370 | }
|
371 | }
|
372 | });
|
373 |
|
374 | cb(frames);
|
375 | });
|
376 | }
|
377 |
|
378 |
|
379 | module.exports.parseStack = parseStack;
|
380 | module.exports.getModule = getModule;
|