posthog-node 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/index.ts +1 -0
- package/lib/index.cjs.js +911 -7
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +44 -3
- package/lib/index.esm.js +911 -7
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +6 -0
- package/lib/posthog-core/src/types.d.ts +1 -0
- package/lib/posthog-core/src/utils.d.ts +2 -0
- package/lib/posthog-node/index.d.ts +1 -0
- package/lib/posthog-node/src/error-tracking.d.ts +12 -0
- package/lib/posthog-node/src/extensions/error-tracking/autocapture.d.ts +3 -0
- package/lib/posthog-node/src/extensions/error-tracking/context-lines.d.ts +4 -0
- package/lib/posthog-node/src/extensions/error-tracking/error-conversion.d.ts +5 -0
- package/lib/posthog-node/src/extensions/error-tracking/reduceable-cache.d.ts +12 -0
- package/lib/posthog-node/src/extensions/error-tracking/stack-trace.d.ts +15 -0
- package/lib/posthog-node/src/extensions/error-tracking/type-checking.d.ts +7 -0
- package/lib/posthog-node/src/extensions/error-tracking/types.d.ts +57 -0
- package/lib/posthog-node/src/extensions/express.d.ts +17 -0
- package/lib/posthog-node/src/extensions/sentry-integration.d.ts +1 -2
- package/lib/posthog-node/src/fetch.d.ts +1 -2
- package/lib/posthog-node/src/posthog-node.d.ts +5 -0
- package/package.json +1 -1
- package/src/error-tracking.ts +66 -0
- package/src/extensions/error-tracking/autocapture.ts +62 -0
- package/src/extensions/error-tracking/context-lines.ts +389 -0
- package/src/extensions/error-tracking/error-conversion.ts +250 -0
- package/src/extensions/error-tracking/reduceable-cache.ts +36 -0
- package/src/extensions/error-tracking/stack-trace.ts +269 -0
- package/src/extensions/error-tracking/type-checking.ts +37 -0
- package/src/extensions/error-tracking/types.ts +62 -0
- package/src/extensions/express.ts +37 -0
- package/src/extensions/sentry-integration.ts +1 -3
- package/src/fetch.ts +3 -7
- package/src/posthog-node.ts +10 -0
package/lib/index.cjs.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
|
+
var node_fs = require('node:fs');
|
|
7
|
+
var node_readline = require('node:readline');
|
|
8
|
+
var node_path = require('node:path');
|
|
6
9
|
|
|
7
|
-
var version = "4.
|
|
10
|
+
var version = "4.5.0";
|
|
8
11
|
|
|
9
12
|
var PostHogPersistedProperty;
|
|
10
13
|
(function (PostHogPersistedProperty) {
|
|
@@ -69,6 +72,9 @@ function safeSetTimeout(fn, timeout) {
|
|
|
69
72
|
// We unref if available to prevent Node.js hanging on exit
|
|
70
73
|
t?.unref && t?.unref();
|
|
71
74
|
return t;
|
|
75
|
+
}
|
|
76
|
+
function getFetch() {
|
|
77
|
+
return typeof fetch !== 'undefined' ? fetch : typeof global.fetch !== 'undefined' ? global.fetch : undefined;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
|
|
@@ -1431,10 +1437,7 @@ class PostHogMemoryStorage {
|
|
|
1431
1437
|
* This is currently solved by using the global fetch if available instead.
|
|
1432
1438
|
* See https://github.com/PostHog/posthog-js-lite/issues/127 for more info
|
|
1433
1439
|
*/
|
|
1434
|
-
let _fetch =
|
|
1435
|
-
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
|
|
1436
|
-
// @ts-ignore
|
|
1437
|
-
typeof fetch !== 'undefined' ? fetch : typeof global.fetch !== 'undefined' ? global.fetch : undefined;
|
|
1440
|
+
let _fetch = getFetch();
|
|
1438
1441
|
if (!_fetch) {
|
|
1439
1442
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1440
1443
|
const axios = require('axios');
|
|
@@ -2040,6 +2043,884 @@ function relativeDateParseForFeatureFlagMatching(value) {
|
|
|
2040
2043
|
}
|
|
2041
2044
|
}
|
|
2042
2045
|
|
|
2046
|
+
function makeUncaughtExceptionHandler(captureFn, onFatalFn) {
|
|
2047
|
+
let calledFatalError = false;
|
|
2048
|
+
return Object.assign(error => {
|
|
2049
|
+
// Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not
|
|
2050
|
+
// want to alter this behaviour so we check for other listeners that users may have attached themselves and adjust
|
|
2051
|
+
// exit behaviour of the SDK accordingly:
|
|
2052
|
+
// - If other listeners are attached, do not exit.
|
|
2053
|
+
// - If the only listener attached is ours, exit.
|
|
2054
|
+
const userProvidedListenersCount = global.process.listeners('uncaughtException').filter(listener => {
|
|
2055
|
+
// There are 2 listeners we ignore:
|
|
2056
|
+
return (
|
|
2057
|
+
// as soon as we're using domains this listener is attached by node itself
|
|
2058
|
+
listener.name !== 'domainUncaughtExceptionClear' &&
|
|
2059
|
+
// the handler we register in this integration
|
|
2060
|
+
listener._posthogErrorHandler !== true
|
|
2061
|
+
);
|
|
2062
|
+
}).length;
|
|
2063
|
+
const processWouldExit = userProvidedListenersCount === 0;
|
|
2064
|
+
captureFn(error, {
|
|
2065
|
+
mechanism: {
|
|
2066
|
+
type: 'onuncaughtexception',
|
|
2067
|
+
handled: false
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
if (!calledFatalError && processWouldExit) {
|
|
2071
|
+
calledFatalError = true;
|
|
2072
|
+
onFatalFn();
|
|
2073
|
+
}
|
|
2074
|
+
}, {
|
|
2075
|
+
_posthogErrorHandler: true
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
function addUncaughtExceptionListener(captureFn, onFatalFn) {
|
|
2079
|
+
global.process.on('uncaughtException', makeUncaughtExceptionHandler(captureFn, onFatalFn));
|
|
2080
|
+
}
|
|
2081
|
+
function addUnhandledRejectionListener(captureFn) {
|
|
2082
|
+
global.process.on('unhandledRejection', reason => {
|
|
2083
|
+
captureFn(reason, {
|
|
2084
|
+
mechanism: {
|
|
2085
|
+
type: 'onunhandledrejection',
|
|
2086
|
+
handled: false
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function isEvent(candidate) {
|
|
2093
|
+
return typeof Event !== 'undefined' && isInstanceOf(candidate, Event);
|
|
2094
|
+
}
|
|
2095
|
+
function isPlainObject(candidate) {
|
|
2096
|
+
return isBuiltin(candidate, 'Object');
|
|
2097
|
+
}
|
|
2098
|
+
function isError(candidate) {
|
|
2099
|
+
switch (Object.prototype.toString.call(candidate)) {
|
|
2100
|
+
case '[object Error]':
|
|
2101
|
+
case '[object Exception]':
|
|
2102
|
+
case '[object DOMException]':
|
|
2103
|
+
case '[object WebAssembly.Exception]':
|
|
2104
|
+
return true;
|
|
2105
|
+
default:
|
|
2106
|
+
return isInstanceOf(candidate, Error);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
function isInstanceOf(candidate, base) {
|
|
2110
|
+
try {
|
|
2111
|
+
return candidate instanceof base;
|
|
2112
|
+
} catch {
|
|
2113
|
+
return false;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function isErrorEvent(event) {
|
|
2117
|
+
return isBuiltin(event, 'ErrorEvent');
|
|
2118
|
+
}
|
|
2119
|
+
function isBuiltin(candidate, className) {
|
|
2120
|
+
return Object.prototype.toString.call(candidate) === `[object ${className}]`;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
/** A simple Least Recently Used map */
|
|
2124
|
+
class ReduceableCache {
|
|
2125
|
+
constructor(_maxSize) {
|
|
2126
|
+
this._maxSize = _maxSize;
|
|
2127
|
+
this._cache = new Map();
|
|
2128
|
+
}
|
|
2129
|
+
/** Get an entry or undefined if it was not in the cache. Re-inserts to update the recently used order */
|
|
2130
|
+
get(key) {
|
|
2131
|
+
const value = this._cache.get(key);
|
|
2132
|
+
if (value === undefined) {
|
|
2133
|
+
return undefined;
|
|
2134
|
+
}
|
|
2135
|
+
// Remove and re-insert to update the order
|
|
2136
|
+
this._cache.delete(key);
|
|
2137
|
+
this._cache.set(key, value);
|
|
2138
|
+
return value;
|
|
2139
|
+
}
|
|
2140
|
+
/** Insert an entry and evict an older entry if we've reached maxSize */
|
|
2141
|
+
set(key, value) {
|
|
2142
|
+
this._cache.set(key, value);
|
|
2143
|
+
}
|
|
2144
|
+
/** Remove an entry and return the entry if it was in the cache */
|
|
2145
|
+
reduce() {
|
|
2146
|
+
while (this._cache.size >= this._maxSize) {
|
|
2147
|
+
const value = this._cache.keys().next().value;
|
|
2148
|
+
if (value) {
|
|
2149
|
+
// keys() returns an iterator in insertion order so keys().next() gives us the oldest key
|
|
2150
|
+
this._cache.delete(value);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
const LRU_FILE_CONTENTS_CACHE = new ReduceableCache(25);
|
|
2157
|
+
const LRU_FILE_CONTENTS_FS_READ_FAILED = new ReduceableCache(20);
|
|
2158
|
+
const DEFAULT_LINES_OF_CONTEXT = 7;
|
|
2159
|
+
// Determines the upper bound of lineno/colno that we will attempt to read. Large colno values are likely to be
|
|
2160
|
+
// minified code while large lineno values are likely to be bundled code.
|
|
2161
|
+
// Exported for testing purposes.
|
|
2162
|
+
const MAX_CONTEXTLINES_COLNO = 1000;
|
|
2163
|
+
const MAX_CONTEXTLINES_LINENO = 10000;
|
|
2164
|
+
async function addSourceContext(frames) {
|
|
2165
|
+
// keep a lookup map of which files we've already enqueued to read,
|
|
2166
|
+
// so we don't enqueue the same file multiple times which would cause multiple i/o reads
|
|
2167
|
+
const filesToLines = {};
|
|
2168
|
+
// Maps preserve insertion order, so we iterate in reverse, starting at the
|
|
2169
|
+
// outermost frame and closer to where the exception has occurred (poor mans priority)
|
|
2170
|
+
for (let i = frames.length - 1; i >= 0; i--) {
|
|
2171
|
+
const frame = frames[i];
|
|
2172
|
+
const filename = frame?.filename;
|
|
2173
|
+
if (!frame || typeof filename !== 'string' || typeof frame.lineno !== 'number' || shouldSkipContextLinesForFile(filename) || shouldSkipContextLinesForFrame(frame)) {
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
const filesToLinesOutput = filesToLines[filename];
|
|
2177
|
+
if (!filesToLinesOutput) {
|
|
2178
|
+
filesToLines[filename] = [];
|
|
2179
|
+
}
|
|
2180
|
+
filesToLines[filename].push(frame.lineno);
|
|
2181
|
+
}
|
|
2182
|
+
const files = Object.keys(filesToLines);
|
|
2183
|
+
if (files.length == 0) {
|
|
2184
|
+
return frames;
|
|
2185
|
+
}
|
|
2186
|
+
const readlinePromises = [];
|
|
2187
|
+
for (const file of files) {
|
|
2188
|
+
// If we failed to read this before, dont try reading it again.
|
|
2189
|
+
if (LRU_FILE_CONTENTS_FS_READ_FAILED.get(file)) {
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
const filesToLineRanges = filesToLines[file];
|
|
2193
|
+
if (!filesToLineRanges) {
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
// Sort ranges so that they are sorted by line increasing order and match how the file is read.
|
|
2197
|
+
filesToLineRanges.sort((a, b) => a - b);
|
|
2198
|
+
// Check if the contents are already in the cache and if we can avoid reading the file again.
|
|
2199
|
+
const ranges = makeLineReaderRanges(filesToLineRanges);
|
|
2200
|
+
if (ranges.every(r => rangeExistsInContentCache(file, r))) {
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
const cache = emplace(LRU_FILE_CONTENTS_CACHE, file, {});
|
|
2204
|
+
readlinePromises.push(getContextLinesFromFile(file, ranges, cache));
|
|
2205
|
+
}
|
|
2206
|
+
// The promise rejections are caught in order to prevent them from short circuiting Promise.all
|
|
2207
|
+
await Promise.all(readlinePromises).catch(() => {});
|
|
2208
|
+
// Perform the same loop as above, but this time we can assume all files are in the cache
|
|
2209
|
+
// and attempt to add source context to frames.
|
|
2210
|
+
if (frames && frames.length > 0) {
|
|
2211
|
+
addSourceContextToFrames(frames, LRU_FILE_CONTENTS_CACHE);
|
|
2212
|
+
}
|
|
2213
|
+
// Once we're finished processing an exception reduce the files held in the cache
|
|
2214
|
+
// so that we don't indefinetly increase the size of this map
|
|
2215
|
+
LRU_FILE_CONTENTS_CACHE.reduce();
|
|
2216
|
+
return frames;
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Extracts lines from a file and stores them in a cache.
|
|
2220
|
+
*/
|
|
2221
|
+
function getContextLinesFromFile(path, ranges, output) {
|
|
2222
|
+
return new Promise(resolve => {
|
|
2223
|
+
// It is important *not* to have any async code between createInterface and the 'line' event listener
|
|
2224
|
+
// as it will cause the 'line' event to
|
|
2225
|
+
// be emitted before the listener is attached.
|
|
2226
|
+
const stream = node_fs.createReadStream(path);
|
|
2227
|
+
const lineReaded = node_readline.createInterface({
|
|
2228
|
+
input: stream
|
|
2229
|
+
});
|
|
2230
|
+
// We need to explicitly destroy the stream to prevent memory leaks,
|
|
2231
|
+
// removing the listeners on the readline interface is not enough.
|
|
2232
|
+
// See: https://github.com/nodejs/node/issues/9002 and https://github.com/getsentry/sentry-javascript/issues/14892
|
|
2233
|
+
function destroyStreamAndResolve() {
|
|
2234
|
+
stream.destroy();
|
|
2235
|
+
resolve();
|
|
2236
|
+
}
|
|
2237
|
+
// Init at zero and increment at the start of the loop because lines are 1 indexed.
|
|
2238
|
+
let lineNumber = 0;
|
|
2239
|
+
let currentRangeIndex = 0;
|
|
2240
|
+
const range = ranges[currentRangeIndex];
|
|
2241
|
+
if (range === undefined) {
|
|
2242
|
+
// We should never reach this point, but if we do, we should resolve the promise to prevent it from hanging.
|
|
2243
|
+
destroyStreamAndResolve();
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
let rangeStart = range[0];
|
|
2247
|
+
let rangeEnd = range[1];
|
|
2248
|
+
// We use this inside Promise.all, so we need to resolve the promise even if there is an error
|
|
2249
|
+
// to prevent Promise.all from short circuiting the rest.
|
|
2250
|
+
function onStreamError() {
|
|
2251
|
+
// Mark file path as failed to read and prevent multiple read attempts.
|
|
2252
|
+
LRU_FILE_CONTENTS_FS_READ_FAILED.set(path, 1);
|
|
2253
|
+
lineReaded.close();
|
|
2254
|
+
lineReaded.removeAllListeners();
|
|
2255
|
+
destroyStreamAndResolve();
|
|
2256
|
+
}
|
|
2257
|
+
// We need to handle the error event to prevent the process from crashing in < Node 16
|
|
2258
|
+
// https://github.com/nodejs/node/pull/31603
|
|
2259
|
+
stream.on('error', onStreamError);
|
|
2260
|
+
lineReaded.on('error', onStreamError);
|
|
2261
|
+
lineReaded.on('close', destroyStreamAndResolve);
|
|
2262
|
+
lineReaded.on('line', line => {
|
|
2263
|
+
lineNumber++;
|
|
2264
|
+
if (lineNumber < rangeStart) {
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
// !Warning: This mutates the cache by storing the snipped line into the cache.
|
|
2268
|
+
output[lineNumber] = snipLine(line, 0);
|
|
2269
|
+
if (lineNumber >= rangeEnd) {
|
|
2270
|
+
if (currentRangeIndex === ranges.length - 1) {
|
|
2271
|
+
// We need to close the file stream and remove listeners, else the reader will continue to run our listener;
|
|
2272
|
+
lineReaded.close();
|
|
2273
|
+
lineReaded.removeAllListeners();
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
currentRangeIndex++;
|
|
2277
|
+
const range = ranges[currentRangeIndex];
|
|
2278
|
+
if (range === undefined) {
|
|
2279
|
+
// This should never happen as it means we have a bug in the context.
|
|
2280
|
+
lineReaded.close();
|
|
2281
|
+
lineReaded.removeAllListeners();
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
rangeStart = range[0];
|
|
2285
|
+
rangeEnd = range[1];
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
/** Adds context lines to frames */
|
|
2291
|
+
function addSourceContextToFrames(frames, cache) {
|
|
2292
|
+
for (const frame of frames) {
|
|
2293
|
+
// Only add context if we have a filename and it hasn't already been added
|
|
2294
|
+
if (frame.filename && frame.context_line === undefined && typeof frame.lineno === 'number') {
|
|
2295
|
+
const contents = cache.get(frame.filename);
|
|
2296
|
+
if (contents === undefined) {
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
addContextToFrame(frame.lineno, frame, contents);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Resolves context lines before and after the given line number and appends them to the frame;
|
|
2305
|
+
*/
|
|
2306
|
+
function addContextToFrame(lineno, frame, contents) {
|
|
2307
|
+
// When there is no line number in the frame, attaching context is nonsensical and will even break grouping.
|
|
2308
|
+
// We already check for lineno before calling this, but since StackFrame lineno is optional, we check it again.
|
|
2309
|
+
if (frame.lineno === undefined || contents === undefined) {
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
frame.pre_context = [];
|
|
2313
|
+
for (let i = makeRangeStart(lineno); i < lineno; i++) {
|
|
2314
|
+
// We always expect the start context as line numbers cannot be negative. If we dont find a line, then
|
|
2315
|
+
// something went wrong somewhere. Clear the context and return without adding any linecontext.
|
|
2316
|
+
const line = contents[i];
|
|
2317
|
+
if (line === undefined) {
|
|
2318
|
+
clearLineContext(frame);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
frame.pre_context.push(line);
|
|
2322
|
+
}
|
|
2323
|
+
// We should always have the context line. If we dont, something went wrong, so we clear the context and return
|
|
2324
|
+
// without adding any linecontext.
|
|
2325
|
+
if (contents[lineno] === undefined) {
|
|
2326
|
+
clearLineContext(frame);
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
frame.context_line = contents[lineno];
|
|
2330
|
+
const end = makeRangeEnd(lineno);
|
|
2331
|
+
frame.post_context = [];
|
|
2332
|
+
for (let i = lineno + 1; i <= end; i++) {
|
|
2333
|
+
// Since we dont track when the file ends, we cant clear the context if we dont find a line as it could
|
|
2334
|
+
// just be that we reached the end of the file.
|
|
2335
|
+
const line = contents[i];
|
|
2336
|
+
if (line === undefined) {
|
|
2337
|
+
break;
|
|
2338
|
+
}
|
|
2339
|
+
frame.post_context.push(line);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Clears the context lines from a frame, used to reset a frame to its original state
|
|
2344
|
+
* if we fail to resolve all context lines for it.
|
|
2345
|
+
*/
|
|
2346
|
+
function clearLineContext(frame) {
|
|
2347
|
+
delete frame.pre_context;
|
|
2348
|
+
delete frame.context_line;
|
|
2349
|
+
delete frame.post_context;
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Determines if context lines should be skipped for a file.
|
|
2353
|
+
* - .min.(mjs|cjs|js) files are and not useful since they dont point to the original source
|
|
2354
|
+
* - node: prefixed modules are part of the runtime and cannot be resolved to a file
|
|
2355
|
+
* - data: skip json, wasm and inline js https://nodejs.org/api/esm.html#data-imports
|
|
2356
|
+
*/
|
|
2357
|
+
function shouldSkipContextLinesForFile(path) {
|
|
2358
|
+
// Test the most common prefix and extension first. These are the ones we
|
|
2359
|
+
// are most likely to see in user applications and are the ones we can break out of first.
|
|
2360
|
+
return path.startsWith('node:') || path.endsWith('.min.js') || path.endsWith('.min.cjs') || path.endsWith('.min.mjs') || path.startsWith('data:');
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Determines if we should skip contextlines based off the max lineno and colno values.
|
|
2364
|
+
*/
|
|
2365
|
+
function shouldSkipContextLinesForFrame(frame) {
|
|
2366
|
+
if (frame.lineno !== undefined && frame.lineno > MAX_CONTEXTLINES_LINENO) {
|
|
2367
|
+
return true;
|
|
2368
|
+
}
|
|
2369
|
+
if (frame.colno !== undefined && frame.colno > MAX_CONTEXTLINES_COLNO) {
|
|
2370
|
+
return true;
|
|
2371
|
+
}
|
|
2372
|
+
return false;
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Checks if we have all the contents that we need in the cache.
|
|
2376
|
+
*/
|
|
2377
|
+
function rangeExistsInContentCache(file, range) {
|
|
2378
|
+
const contents = LRU_FILE_CONTENTS_CACHE.get(file);
|
|
2379
|
+
if (contents === undefined) {
|
|
2380
|
+
return false;
|
|
2381
|
+
}
|
|
2382
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
2383
|
+
if (contents[i] === undefined) {
|
|
2384
|
+
return false;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Creates contiguous ranges of lines to read from a file. In the case where context lines overlap,
|
|
2391
|
+
* the ranges are merged to create a single range.
|
|
2392
|
+
*/
|
|
2393
|
+
function makeLineReaderRanges(lines) {
|
|
2394
|
+
if (!lines.length) {
|
|
2395
|
+
return [];
|
|
2396
|
+
}
|
|
2397
|
+
let i = 0;
|
|
2398
|
+
const line = lines[0];
|
|
2399
|
+
if (typeof line !== 'number') {
|
|
2400
|
+
return [];
|
|
2401
|
+
}
|
|
2402
|
+
let current = makeContextRange(line);
|
|
2403
|
+
const out = [];
|
|
2404
|
+
while (true) {
|
|
2405
|
+
if (i === lines.length - 1) {
|
|
2406
|
+
out.push(current);
|
|
2407
|
+
break;
|
|
2408
|
+
}
|
|
2409
|
+
// If the next line falls into the current range, extend the current range to lineno + linecontext.
|
|
2410
|
+
const next = lines[i + 1];
|
|
2411
|
+
if (typeof next !== 'number') {
|
|
2412
|
+
break;
|
|
2413
|
+
}
|
|
2414
|
+
if (next <= current[1]) {
|
|
2415
|
+
current[1] = next + DEFAULT_LINES_OF_CONTEXT;
|
|
2416
|
+
} else {
|
|
2417
|
+
out.push(current);
|
|
2418
|
+
current = makeContextRange(next);
|
|
2419
|
+
}
|
|
2420
|
+
i++;
|
|
2421
|
+
}
|
|
2422
|
+
return out;
|
|
2423
|
+
}
|
|
2424
|
+
// Determine start and end indices for context range (inclusive);
|
|
2425
|
+
function makeContextRange(line) {
|
|
2426
|
+
return [makeRangeStart(line), makeRangeEnd(line)];
|
|
2427
|
+
}
|
|
2428
|
+
// Compute inclusive end context range
|
|
2429
|
+
function makeRangeStart(line) {
|
|
2430
|
+
return Math.max(1, line - DEFAULT_LINES_OF_CONTEXT);
|
|
2431
|
+
}
|
|
2432
|
+
// Compute inclusive start context range
|
|
2433
|
+
function makeRangeEnd(line) {
|
|
2434
|
+
return line + DEFAULT_LINES_OF_CONTEXT;
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* Get or init map value
|
|
2438
|
+
*/
|
|
2439
|
+
function emplace(map, key, contents) {
|
|
2440
|
+
const value = map.get(key);
|
|
2441
|
+
if (value === undefined) {
|
|
2442
|
+
map.set(key, contents);
|
|
2443
|
+
return contents;
|
|
2444
|
+
}
|
|
2445
|
+
return value;
|
|
2446
|
+
}
|
|
2447
|
+
function snipLine(line, colno) {
|
|
2448
|
+
let newLine = line;
|
|
2449
|
+
const lineLength = newLine.length;
|
|
2450
|
+
if (lineLength <= 150) {
|
|
2451
|
+
return newLine;
|
|
2452
|
+
}
|
|
2453
|
+
if (colno > lineLength) {
|
|
2454
|
+
colno = lineLength;
|
|
2455
|
+
}
|
|
2456
|
+
let start = Math.max(colno - 60, 0);
|
|
2457
|
+
if (start < 5) {
|
|
2458
|
+
start = 0;
|
|
2459
|
+
}
|
|
2460
|
+
let end = Math.min(start + 140, lineLength);
|
|
2461
|
+
if (end > lineLength - 5) {
|
|
2462
|
+
end = lineLength;
|
|
2463
|
+
}
|
|
2464
|
+
if (end === lineLength) {
|
|
2465
|
+
start = Math.max(end - 140, 0);
|
|
2466
|
+
}
|
|
2467
|
+
newLine = newLine.slice(start, end);
|
|
2468
|
+
if (start > 0) {
|
|
2469
|
+
newLine = `...${newLine}`;
|
|
2470
|
+
}
|
|
2471
|
+
if (end < lineLength) {
|
|
2472
|
+
newLine += '...';
|
|
2473
|
+
}
|
|
2474
|
+
return newLine;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* based on the very wonderful MIT licensed Sentry SDK
|
|
2479
|
+
*/
|
|
2480
|
+
async function propertiesFromUnknownInput(stackParser, input, hint) {
|
|
2481
|
+
const providedMechanism = hint && hint.mechanism;
|
|
2482
|
+
const mechanism = providedMechanism || {
|
|
2483
|
+
handled: true,
|
|
2484
|
+
type: 'generic'
|
|
2485
|
+
};
|
|
2486
|
+
const error = getError(mechanism, input, hint);
|
|
2487
|
+
const exception = await exceptionFromError(stackParser, error);
|
|
2488
|
+
exception.value = exception.value || '';
|
|
2489
|
+
exception.type = exception.type || 'Error';
|
|
2490
|
+
exception.mechanism = mechanism;
|
|
2491
|
+
const properties = {
|
|
2492
|
+
$exception_list: [exception]
|
|
2493
|
+
};
|
|
2494
|
+
return properties;
|
|
2495
|
+
}
|
|
2496
|
+
function getError(mechanism, exception, hint) {
|
|
2497
|
+
if (isError(exception)) {
|
|
2498
|
+
return exception;
|
|
2499
|
+
}
|
|
2500
|
+
mechanism.synthetic = true;
|
|
2501
|
+
if (isPlainObject(exception)) {
|
|
2502
|
+
const errorFromProp = getErrorPropertyFromObject(exception);
|
|
2503
|
+
if (errorFromProp) {
|
|
2504
|
+
return errorFromProp;
|
|
2505
|
+
}
|
|
2506
|
+
const message = getMessageForObject(exception);
|
|
2507
|
+
const ex = hint?.syntheticException || new Error(message);
|
|
2508
|
+
ex.message = message;
|
|
2509
|
+
return ex;
|
|
2510
|
+
}
|
|
2511
|
+
// This handles when someone does: `throw "something awesome";`
|
|
2512
|
+
// We use synthesized Error here so we can extract a (rough) stack trace.
|
|
2513
|
+
const ex = hint?.syntheticException || new Error(exception);
|
|
2514
|
+
ex.message = `${exception}`;
|
|
2515
|
+
return ex;
|
|
2516
|
+
}
|
|
2517
|
+
/** If a plain object has a property that is an `Error`, return this error. */
|
|
2518
|
+
function getErrorPropertyFromObject(obj) {
|
|
2519
|
+
for (const prop in obj) {
|
|
2520
|
+
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
2521
|
+
const value = obj[prop];
|
|
2522
|
+
if (value instanceof Error) {
|
|
2523
|
+
return value;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return undefined;
|
|
2528
|
+
}
|
|
2529
|
+
function getMessageForObject(exception) {
|
|
2530
|
+
if ('name' in exception && typeof exception.name === 'string') {
|
|
2531
|
+
let message = `'${exception.name}' captured as exception`;
|
|
2532
|
+
if ('message' in exception && typeof exception.message === 'string') {
|
|
2533
|
+
message += ` with message '${exception.message}'`;
|
|
2534
|
+
}
|
|
2535
|
+
return message;
|
|
2536
|
+
} else if ('message' in exception && typeof exception.message === 'string') {
|
|
2537
|
+
return exception.message;
|
|
2538
|
+
}
|
|
2539
|
+
const keys = extractExceptionKeysForMessage(exception);
|
|
2540
|
+
// Some ErrorEvent instances do not have an `error` property, which is why they are not handled before
|
|
2541
|
+
// We still want to try to get a decent message for these cases
|
|
2542
|
+
if (isErrorEvent(exception)) {
|
|
2543
|
+
return `Event \`ErrorEvent\` captured as exception with message \`${exception.message}\``;
|
|
2544
|
+
}
|
|
2545
|
+
const className = getObjectClassName(exception);
|
|
2546
|
+
return `${className && className !== 'Object' ? `'${className}'` : 'Object'} captured as exception with keys: ${keys}`;
|
|
2547
|
+
}
|
|
2548
|
+
function getObjectClassName(obj) {
|
|
2549
|
+
try {
|
|
2550
|
+
const prototype = Object.getPrototypeOf(obj);
|
|
2551
|
+
return prototype ? prototype.constructor.name : undefined;
|
|
2552
|
+
} catch (e) {
|
|
2553
|
+
// ignore errors here
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Given any captured exception, extract its keys and create a sorted
|
|
2558
|
+
* and truncated list that will be used inside the event message.
|
|
2559
|
+
* eg. `Non-error exception captured with keys: foo, bar, baz`
|
|
2560
|
+
*/
|
|
2561
|
+
function extractExceptionKeysForMessage(exception, maxLength = 40) {
|
|
2562
|
+
const keys = Object.keys(convertToPlainObject(exception));
|
|
2563
|
+
keys.sort();
|
|
2564
|
+
const firstKey = keys[0];
|
|
2565
|
+
if (!firstKey) {
|
|
2566
|
+
return '[object has no keys]';
|
|
2567
|
+
}
|
|
2568
|
+
if (firstKey.length >= maxLength) {
|
|
2569
|
+
return truncate(firstKey, maxLength);
|
|
2570
|
+
}
|
|
2571
|
+
for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) {
|
|
2572
|
+
const serialized = keys.slice(0, includedKeys).join(', ');
|
|
2573
|
+
if (serialized.length > maxLength) {
|
|
2574
|
+
continue;
|
|
2575
|
+
}
|
|
2576
|
+
if (includedKeys === keys.length) {
|
|
2577
|
+
return serialized;
|
|
2578
|
+
}
|
|
2579
|
+
return truncate(serialized, maxLength);
|
|
2580
|
+
}
|
|
2581
|
+
return '';
|
|
2582
|
+
}
|
|
2583
|
+
function truncate(str, max = 0) {
|
|
2584
|
+
if (typeof str !== 'string' || max === 0) {
|
|
2585
|
+
return str;
|
|
2586
|
+
}
|
|
2587
|
+
return str.length <= max ? str : `${str.slice(0, max)}...`;
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
|
|
2591
|
+
* non-enumerable properties attached.
|
|
2592
|
+
*
|
|
2593
|
+
* @param value Initial source that we have to transform in order for it to be usable by the serializer
|
|
2594
|
+
* @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor
|
|
2595
|
+
* an Error.
|
|
2596
|
+
*/
|
|
2597
|
+
function convertToPlainObject(value) {
|
|
2598
|
+
if (isError(value)) {
|
|
2599
|
+
return {
|
|
2600
|
+
message: value.message,
|
|
2601
|
+
name: value.name,
|
|
2602
|
+
stack: value.stack,
|
|
2603
|
+
...getOwnProperties(value)
|
|
2604
|
+
};
|
|
2605
|
+
} else if (isEvent(value)) {
|
|
2606
|
+
const newObj = {
|
|
2607
|
+
type: value.type,
|
|
2608
|
+
target: serializeEventTarget(value.target),
|
|
2609
|
+
currentTarget: serializeEventTarget(value.currentTarget),
|
|
2610
|
+
...getOwnProperties(value)
|
|
2611
|
+
};
|
|
2612
|
+
// TODO: figure out why this fails typing (I think CustomEvent is only supported in Node 19 onwards)
|
|
2613
|
+
// if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) {
|
|
2614
|
+
// newObj.detail = (value as unknown as CustomEvent).detail
|
|
2615
|
+
// }
|
|
2616
|
+
return newObj;
|
|
2617
|
+
} else {
|
|
2618
|
+
return value;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
/** Filters out all but an object's own properties */
|
|
2622
|
+
function getOwnProperties(obj) {
|
|
2623
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
2624
|
+
const extractedProps = {};
|
|
2625
|
+
for (const property in obj) {
|
|
2626
|
+
if (Object.prototype.hasOwnProperty.call(obj, property)) {
|
|
2627
|
+
extractedProps[property] = obj[property];
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
return extractedProps;
|
|
2631
|
+
} else {
|
|
2632
|
+
return {};
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
/** Creates a string representation of the target of an `Event` object */
|
|
2636
|
+
function serializeEventTarget(target) {
|
|
2637
|
+
try {
|
|
2638
|
+
return Object.prototype.toString.call(target);
|
|
2639
|
+
} catch (_oO) {
|
|
2640
|
+
return '<unknown>';
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Extracts stack frames from the error and builds an Exception
|
|
2645
|
+
*/
|
|
2646
|
+
async function exceptionFromError(stackParser, error) {
|
|
2647
|
+
const exception = {
|
|
2648
|
+
type: error.name || error.constructor.name,
|
|
2649
|
+
value: error.message
|
|
2650
|
+
};
|
|
2651
|
+
const frames = await addSourceContext(parseStackFrames(stackParser, error));
|
|
2652
|
+
if (frames.length) {
|
|
2653
|
+
exception.stacktrace = {
|
|
2654
|
+
frames,
|
|
2655
|
+
type: 'raw'
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
return exception;
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Extracts stack frames from the error.stack string
|
|
2662
|
+
*/
|
|
2663
|
+
function parseStackFrames(stackParser, error) {
|
|
2664
|
+
return stackParser(error.stack || '', 1);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// copied and adapted from https://github.com/getsentry/sentry-javascript/blob/41fef4b10f3a644179b77985f00f8696c908539f/packages/browser/src/stack-parsers.ts
|
|
2668
|
+
// This was originally forked from https://github.com/csnover/TraceKit, and was largely
|
|
2669
|
+
// re-written as part of raven - js.
|
|
2670
|
+
//
|
|
2671
|
+
// This code was later copied to the JavaScript mono - repo and further modified and
|
|
2672
|
+
// refactored over the years.
|
|
2673
|
+
// Copyright (c) 2013 Onur Can Cakmak onur.cakmak@gmail.com and all TraceKit contributors.
|
|
2674
|
+
//
|
|
2675
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
2676
|
+
// software and associated documentation files(the 'Software'), to deal in the Software
|
|
2677
|
+
// without restriction, including without limitation the rights to use, copy, modify,
|
|
2678
|
+
// merge, publish, distribute, sublicense, and / or sell copies of the Software, and to
|
|
2679
|
+
// permit persons to whom the Software is furnished to do so, subject to the following
|
|
2680
|
+
// conditions:
|
|
2681
|
+
//
|
|
2682
|
+
// The above copyright notice and this permission notice shall be included in all copies
|
|
2683
|
+
// or substantial portions of the Software.
|
|
2684
|
+
//
|
|
2685
|
+
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
2686
|
+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
2687
|
+
// PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
2688
|
+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
2689
|
+
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
2690
|
+
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
2691
|
+
const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/;
|
|
2692
|
+
const STACKTRACE_FRAME_LIMIT = 50;
|
|
2693
|
+
const UNKNOWN_FUNCTION = '?';
|
|
2694
|
+
/** Node Stack line parser */
|
|
2695
|
+
function node(getModule) {
|
|
2696
|
+
const FILENAME_MATCH = /^\s*[-]{4,}$/;
|
|
2697
|
+
const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/;
|
|
2698
|
+
return line => {
|
|
2699
|
+
const lineMatch = line.match(FULL_MATCH);
|
|
2700
|
+
if (lineMatch) {
|
|
2701
|
+
let object;
|
|
2702
|
+
let method;
|
|
2703
|
+
let functionName;
|
|
2704
|
+
let typeName;
|
|
2705
|
+
let methodName;
|
|
2706
|
+
if (lineMatch[1]) {
|
|
2707
|
+
functionName = lineMatch[1];
|
|
2708
|
+
let methodStart = functionName.lastIndexOf('.');
|
|
2709
|
+
if (functionName[methodStart - 1] === '.') {
|
|
2710
|
+
methodStart--;
|
|
2711
|
+
}
|
|
2712
|
+
if (methodStart > 0) {
|
|
2713
|
+
object = functionName.slice(0, methodStart);
|
|
2714
|
+
method = functionName.slice(methodStart + 1);
|
|
2715
|
+
const objectEnd = object.indexOf('.Module');
|
|
2716
|
+
if (objectEnd > 0) {
|
|
2717
|
+
functionName = functionName.slice(objectEnd + 1);
|
|
2718
|
+
object = object.slice(0, objectEnd);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
typeName = undefined;
|
|
2722
|
+
}
|
|
2723
|
+
if (method) {
|
|
2724
|
+
typeName = object;
|
|
2725
|
+
methodName = method;
|
|
2726
|
+
}
|
|
2727
|
+
if (method === '<anonymous>') {
|
|
2728
|
+
methodName = undefined;
|
|
2729
|
+
functionName = undefined;
|
|
2730
|
+
}
|
|
2731
|
+
if (functionName === undefined) {
|
|
2732
|
+
methodName = methodName || UNKNOWN_FUNCTION;
|
|
2733
|
+
functionName = typeName ? `${typeName}.${methodName}` : methodName;
|
|
2734
|
+
}
|
|
2735
|
+
let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2];
|
|
2736
|
+
const isNative = lineMatch[5] === 'native';
|
|
2737
|
+
// If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
|
|
2738
|
+
if (filename?.match(/\/[A-Z]:/)) {
|
|
2739
|
+
filename = filename.slice(1);
|
|
2740
|
+
}
|
|
2741
|
+
if (!filename && lineMatch[5] && !isNative) {
|
|
2742
|
+
filename = lineMatch[5];
|
|
2743
|
+
}
|
|
2744
|
+
return {
|
|
2745
|
+
filename: filename ? decodeURI(filename) : undefined,
|
|
2746
|
+
module: getModule ? getModule(filename) : undefined,
|
|
2747
|
+
function: functionName,
|
|
2748
|
+
lineno: _parseIntOrUndefined(lineMatch[3]),
|
|
2749
|
+
colno: _parseIntOrUndefined(lineMatch[4]),
|
|
2750
|
+
in_app: filenameIsInApp(filename || '', isNative),
|
|
2751
|
+
platform: 'node:javascript'
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
if (line.match(FILENAME_MATCH)) {
|
|
2755
|
+
return {
|
|
2756
|
+
filename: line,
|
|
2757
|
+
platform: 'node:javascript'
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
return undefined;
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Does this filename look like it's part of the app code?
|
|
2765
|
+
*/
|
|
2766
|
+
function filenameIsInApp(filename, isNative = false) {
|
|
2767
|
+
const isInternal = isNative || filename &&
|
|
2768
|
+
// It's not internal if it's an absolute linux path
|
|
2769
|
+
!filename.startsWith('/') &&
|
|
2770
|
+
// It's not internal if it's an absolute windows path
|
|
2771
|
+
!filename.match(/^[A-Z]:/) &&
|
|
2772
|
+
// It's not internal if the path is starting with a dot
|
|
2773
|
+
!filename.startsWith('.') &&
|
|
2774
|
+
// It's not internal if the frame has a protocol. In node, this is usually the case if the file got pre-processed with a bundler like webpack
|
|
2775
|
+
!filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//); // Schema from: https://stackoverflow.com/a/3641782
|
|
2776
|
+
// in_app is all that's not an internal Node function or a module within node_modules
|
|
2777
|
+
// note that isNative appears to return true even for node core libraries
|
|
2778
|
+
// see https://github.com/getsentry/raven-node/issues/176
|
|
2779
|
+
return !isInternal && filename !== undefined && !filename.includes('node_modules/');
|
|
2780
|
+
}
|
|
2781
|
+
function _parseIntOrUndefined(input) {
|
|
2782
|
+
return parseInt(input || '', 10) || undefined;
|
|
2783
|
+
}
|
|
2784
|
+
function nodeStackLineParser(getModule) {
|
|
2785
|
+
return [90, node(getModule)];
|
|
2786
|
+
}
|
|
2787
|
+
const defaultStackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename()));
|
|
2788
|
+
/** Creates a function that gets the module name from a filename */
|
|
2789
|
+
function createGetModuleFromFilename(basePath = process.argv[1] ? node_path.dirname(process.argv[1]) : process.cwd(), isWindows = node_path.sep === '\\') {
|
|
2790
|
+
const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
|
|
2791
|
+
return filename => {
|
|
2792
|
+
if (!filename) {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename;
|
|
2796
|
+
// eslint-disable-next-line prefer-const
|
|
2797
|
+
let {
|
|
2798
|
+
dir,
|
|
2799
|
+
base: file,
|
|
2800
|
+
ext
|
|
2801
|
+
} = node_path.posix.parse(normalizedFilename);
|
|
2802
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
2803
|
+
file = file.slice(0, ext.length * -1);
|
|
2804
|
+
}
|
|
2805
|
+
// The file name might be URI-encoded which we want to decode to
|
|
2806
|
+
// the original file name.
|
|
2807
|
+
const decodedFile = decodeURIComponent(file);
|
|
2808
|
+
if (!dir) {
|
|
2809
|
+
// No dirname whatsoever
|
|
2810
|
+
dir = '.';
|
|
2811
|
+
}
|
|
2812
|
+
const n = dir.lastIndexOf('/node_modules');
|
|
2813
|
+
if (n > -1) {
|
|
2814
|
+
return `${dir.slice(n + 14).replace(/\//g, '.')}:${decodedFile}`;
|
|
2815
|
+
}
|
|
2816
|
+
// Let's see if it's a part of the main module
|
|
2817
|
+
// To be a part of main module, it has to share the same base
|
|
2818
|
+
if (dir.startsWith(normalizedBase)) {
|
|
2819
|
+
const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.');
|
|
2820
|
+
return moduleName ? `${moduleName}:${decodedFile}` : decodedFile;
|
|
2821
|
+
}
|
|
2822
|
+
return decodedFile;
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
/** normalizes Windows paths */
|
|
2826
|
+
function normalizeWindowsPath(path) {
|
|
2827
|
+
return path.replace(/^[A-Z]:/, '') // remove Windows-style prefix
|
|
2828
|
+
.replace(/\\/g, '/'); // replace all `\` instances with `/`
|
|
2829
|
+
}
|
|
2830
|
+
function createStackParser(...parsers) {
|
|
2831
|
+
const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]);
|
|
2832
|
+
return (stack, skipFirstLines = 0) => {
|
|
2833
|
+
const frames = [];
|
|
2834
|
+
const lines = stack.split('\n');
|
|
2835
|
+
for (let i = skipFirstLines; i < lines.length; i++) {
|
|
2836
|
+
const line = lines[i];
|
|
2837
|
+
// Ignore lines over 1kb as they are unlikely to be stack frames.
|
|
2838
|
+
if (line.length > 1024) {
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2841
|
+
// https://github.com/getsentry/sentry-javascript/issues/5459
|
|
2842
|
+
// Remove webpack (error: *) wrappers
|
|
2843
|
+
const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line;
|
|
2844
|
+
// https://github.com/getsentry/sentry-javascript/issues/7813
|
|
2845
|
+
// Skip Error: lines
|
|
2846
|
+
if (cleanedLine.match(/\S*Error: /)) {
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
for (const parser of sortedParsers) {
|
|
2850
|
+
const frame = parser(cleanedLine);
|
|
2851
|
+
if (frame) {
|
|
2852
|
+
frames.push(frame);
|
|
2853
|
+
break;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
if (frames.length >= STACKTRACE_FRAME_LIMIT) {
|
|
2857
|
+
break;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
return reverseAndStripFrames(frames);
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
function reverseAndStripFrames(stack) {
|
|
2864
|
+
if (!stack.length) {
|
|
2865
|
+
return [];
|
|
2866
|
+
}
|
|
2867
|
+
const localStack = Array.from(stack);
|
|
2868
|
+
localStack.reverse();
|
|
2869
|
+
return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map(frame => ({
|
|
2870
|
+
...frame,
|
|
2871
|
+
filename: frame.filename || getLastStackFrame(localStack).filename,
|
|
2872
|
+
function: frame.function || UNKNOWN_FUNCTION
|
|
2873
|
+
}));
|
|
2874
|
+
}
|
|
2875
|
+
function getLastStackFrame(arr) {
|
|
2876
|
+
return arr[arr.length - 1] || {};
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
const SHUTDOWN_TIMEOUT = 2000;
|
|
2880
|
+
class ErrorTracking {
|
|
2881
|
+
static async captureException(client, error, distinctId, hint, additionalProperties) {
|
|
2882
|
+
const properties = {
|
|
2883
|
+
...additionalProperties
|
|
2884
|
+
};
|
|
2885
|
+
if (!distinctId) {
|
|
2886
|
+
properties.$process_person_profile = false;
|
|
2887
|
+
}
|
|
2888
|
+
const exceptionProperties = await propertiesFromUnknownInput(defaultStackParser, error, hint);
|
|
2889
|
+
client.capture({
|
|
2890
|
+
event: '$exception',
|
|
2891
|
+
distinctId: distinctId || uuidv7(),
|
|
2892
|
+
properties: {
|
|
2893
|
+
...exceptionProperties,
|
|
2894
|
+
...properties
|
|
2895
|
+
}
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
constructor(client, options) {
|
|
2899
|
+
this.client = client;
|
|
2900
|
+
this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
|
|
2901
|
+
this.startAutocaptureIfEnabled();
|
|
2902
|
+
}
|
|
2903
|
+
startAutocaptureIfEnabled() {
|
|
2904
|
+
if (this.isEnabled()) {
|
|
2905
|
+
addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this));
|
|
2906
|
+
addUnhandledRejectionListener(this.onException.bind(this));
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
onException(exception, hint) {
|
|
2910
|
+
// Given stateless nature of Node SDK we capture exceptions using personless processing
|
|
2911
|
+
// when no user can be determined e.g. in the case of exception autocapture
|
|
2912
|
+
ErrorTracking.captureException(this.client, exception, uuidv7(), hint, {
|
|
2913
|
+
$process_person_profile: false
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
async onFatalError() {
|
|
2917
|
+
await this.client.shutdown(SHUTDOWN_TIMEOUT);
|
|
2918
|
+
}
|
|
2919
|
+
isEnabled() {
|
|
2920
|
+
return !this.client.isDisabled && this._exceptionAutocaptureEnabled;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2043
2924
|
const THIRTY_SECONDS = 30 * 1000;
|
|
2044
2925
|
const MAX_CACHE_SIZE = 50 * 1000;
|
|
2045
2926
|
// The actual exported Nodejs API.
|
|
@@ -2062,6 +2943,7 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2062
2943
|
customHeaders: this.getCustomHeaders()
|
|
2063
2944
|
});
|
|
2064
2945
|
}
|
|
2946
|
+
this.errorTracking = new ErrorTracking(this, options);
|
|
2065
2947
|
this.distinctIdHasSentFlagCalls = {};
|
|
2066
2948
|
this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
|
|
2067
2949
|
}
|
|
@@ -2365,6 +3247,12 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2365
3247
|
allGroupProperties
|
|
2366
3248
|
};
|
|
2367
3249
|
}
|
|
3250
|
+
captureException(error, distinctId, additionalProperties) {
|
|
3251
|
+
const syntheticException = new Error('PostHog syntheticException');
|
|
3252
|
+
ErrorTracking.captureException(this, error, distinctId, {
|
|
3253
|
+
syntheticException
|
|
3254
|
+
}, additionalProperties);
|
|
3255
|
+
}
|
|
2368
3256
|
}
|
|
2369
3257
|
|
|
2370
3258
|
/**
|
|
@@ -2390,7 +3278,6 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2390
3278
|
* @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
|
|
2391
3279
|
* @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
|
|
2392
3280
|
*/
|
|
2393
|
-
const severityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'];
|
|
2394
3281
|
const NAME = 'posthog-node';
|
|
2395
3282
|
function createEventProcessor(_posthog, {
|
|
2396
3283
|
organization,
|
|
@@ -2475,9 +3362,26 @@ class PostHogSentryIntegration {
|
|
|
2475
3362
|
}
|
|
2476
3363
|
PostHogSentryIntegration.POSTHOG_ID_TAG = 'posthog_distinct_id';
|
|
2477
3364
|
|
|
3365
|
+
function setupExpressErrorHandler(_posthog, app) {
|
|
3366
|
+
app.use((error, _, __, next) => {
|
|
3367
|
+
const hint = {
|
|
3368
|
+
mechanism: {
|
|
3369
|
+
type: 'middleware',
|
|
3370
|
+
handled: false
|
|
3371
|
+
}
|
|
3372
|
+
};
|
|
3373
|
+
// Given stateless nature of Node SDK we capture exceptions using personless processing
|
|
3374
|
+
// when no user can be determined e.g. in the case of exception autocapture
|
|
3375
|
+
ErrorTracking.captureException(_posthog, error, uuidv7(), hint, {
|
|
3376
|
+
$process_person_profile: false
|
|
3377
|
+
});
|
|
3378
|
+
next(error);
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
3381
|
+
|
|
2478
3382
|
exports.PostHog = PostHog;
|
|
2479
3383
|
exports.PostHogSentryIntegration = PostHogSentryIntegration;
|
|
2480
3384
|
exports.createEventProcessor = createEventProcessor;
|
|
2481
3385
|
exports.sentryIntegration = sentryIntegration;
|
|
2482
|
-
exports.
|
|
3386
|
+
exports.setupExpressErrorHandler = setupExpressErrorHandler;
|
|
2483
3387
|
//# sourceMappingURL=index.cjs.js.map
|