@oneuptime/common 10.5.17 → 10.5.18

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.
Files changed (65) hide show
  1. package/Server/API/TelemetryAPI.ts +6 -0
  2. package/Server/EnvironmentConfig.ts +27 -0
  3. package/Server/Infrastructure/ClickhouseDatabase.ts +21 -1
  4. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +19 -0
  5. package/Server/Infrastructure/PostgresDatabase.ts +27 -1
  6. package/Server/Infrastructure/QueueWorker.ts +14 -3
  7. package/Server/Infrastructure/Redis.ts +11 -0
  8. package/Server/Services/TelemetryAttributeService.ts +38 -2
  9. package/Server/Utils/Express.ts +32 -0
  10. package/Server/Utils/GracefulShutdown.ts +194 -0
  11. package/Server/Utils/Monitor/MonitorLogUtil.ts +22 -17
  12. package/Server/Utils/Profiling.ts +14 -6
  13. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +289 -0
  14. package/Server/Utils/Telemetry/StackTraceParser.ts +423 -0
  15. package/Server/Utils/Telemetry.ts +15 -5
  16. package/Tests/Server/Services/TelemetryAttributeService.test.ts +83 -0
  17. package/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.ts +0 -0
  18. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -1
  19. package/UI/Components/Dictionary/Dictionary.tsx +19 -0
  20. package/UI/Components/Filters/FiltersForm.tsx +1 -0
  21. package/UI/Components/Filters/JSONFilter.tsx +2 -0
  22. package/UI/Components/Filters/Types/Filter.ts +1 -0
  23. package/build/dist/Server/API/TelemetryAPI.js +4 -0
  24. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  25. package/build/dist/Server/EnvironmentConfig.js +19 -0
  26. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  27. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js +16 -2
  28. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js.map +1 -1
  29. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +10 -9
  30. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  31. package/build/dist/Server/Infrastructure/PostgresDatabase.js +20 -1
  32. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  33. package/build/dist/Server/Infrastructure/QueueWorker.js +9 -2
  34. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  35. package/build/dist/Server/Infrastructure/Redis.js +5 -0
  36. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  37. package/build/dist/Server/Services/TelemetryAttributeService.js +23 -1
  38. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  39. package/build/dist/Server/Utils/Express.js +23 -0
  40. package/build/dist/Server/Utils/Express.js.map +1 -1
  41. package/build/dist/Server/Utils/GracefulShutdown.js +145 -0
  42. package/build/dist/Server/Utils/GracefulShutdown.js.map +1 -0
  43. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -10
  44. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  45. package/build/dist/Server/Utils/Profiling.js +8 -3
  46. package/build/dist/Server/Utils/Profiling.js.map +1 -1
  47. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +214 -0
  48. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -0
  49. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js +365 -0
  50. package/build/dist/Server/Utils/Telemetry/StackTraceParser.js.map +1 -0
  51. package/build/dist/Server/Utils/Telemetry.js +10 -4
  52. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  53. package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js +50 -0
  54. package/build/dist/Tests/Server/Services/TelemetryAttributeService.test.js.map +1 -0
  55. package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js +0 -0
  56. package/build/dist/Tests/Server/Utils/Telemetry/LogExceptionExtractor.test.js.map +1 -0
  57. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +7 -1
  58. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  59. package/build/dist/UI/Components/Dictionary/Dictionary.js +10 -0
  60. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  61. package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
  62. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  63. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  64. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  65. package/package.json +1 -1
@@ -0,0 +1,289 @@
1
+ import { JSONObject, JSONValue } from "../../../Types/JSON";
2
+ import StackTraceParser, { ParsedStackTrace } from "./StackTraceParser";
3
+
4
+ /**
5
+ * Result of detecting an exception inside a single log record. Shaped to feed
6
+ * the same ExceptionInstance (ClickHouse) + TelemetryException (Postgres)
7
+ * sinks the trace span-event path uses, so log-derived and span-derived
8
+ * exceptions group under one fingerprint when identical.
9
+ */
10
+ export interface ExtractedLogException {
11
+ message: string;
12
+ exceptionType: string;
13
+ stackTrace: string;
14
+ parsedFrames: string; // JSON.stringify(StackFrame[]) or "[]"
15
+ escaped: boolean | null; // Path A may carry exception.escaped; Path B => null (unknown)
16
+ }
17
+
18
+ export interface LogExceptionExtractorInput {
19
+ body: string; // post-scrub log body
20
+ attributes: JSONObject; // post-scrub merged log attributes
21
+ severityNumber: number;
22
+ /**
23
+ * True when the log carries BOTH a traceId and a spanId — i.e. it was
24
+ * emitted inside an instrumented span. The span-exception path is the
25
+ * canonical source for those, so Path B (body scan) is suppressed to avoid
26
+ * double-counting (which would also inflate occuranceCount and the windowed
27
+ * exception monitor). Path A (explicit exception.* attributes) is NOT
28
+ * suppressed — those are an intentional structured exception record.
29
+ */
30
+ hasTraceAndSpan: boolean;
31
+ }
32
+
33
+ /**
34
+ * OTel log severityNumber >= 17 is ERROR (17-20) or FATAL (21-24). Path B only
35
+ * scans those — the overwhelming majority of logs are below this and never
36
+ * reach the parser.
37
+ */
38
+ const MIN_ERROR_SEVERITY_NUMBER: number = 17;
39
+
40
+ /**
41
+ * Only the first 16 KB of a body is parsed. A clean single-record stack trace
42
+ * fits comfortably (~150 frames); the top frames are the most diagnostic and
43
+ * are at the front. Guards against pathological multi-megabyte error logs on
44
+ * the hot path.
45
+ */
46
+ const MAX_PARSE_BODY_LENGTH: number = 16 * 1024;
47
+
48
+ /*
49
+ * Raw log bodies are unbounded (unlike SDK-bounded span exception.stacktrace),
50
+ * so clamp what we store into the ZSTD stackTrace column and the Postgres summary.
51
+ */
52
+ const MAX_STORED_STACK_TRACE_LENGTH: number = 64 * 1024;
53
+ const MAX_MESSAGE_LENGTH: number = 1024;
54
+
55
+ /**
56
+ * Single pre-compiled signature for "this body plausibly contains a stack
57
+ * trace". Evaluated once before the (more expensive) multi-language parser.
58
+ * Covers: Python traceback header, JS/Java `at file:line`, Go panic/goroutine,
59
+ * Python `File "...", line N`, and a typed `SomethingException`/`SomethingError`.
60
+ */
61
+ const LOOKS_LIKE_STACK_TRACE: RegExp =
62
+ /(?:Traceback \(most recent call last\)|\n\s+at\s+.+:\d+|\bpanic:\s|goroutine\s+\d+\s+\[|\n\s*File\s+"[^"]+",\s+line\s+\d+|\b[A-Za-z_][\w.$]*(?:Exception|Error)\b)/;
63
+
64
+ // Header parsers for deriving exceptionType + message from a raw body.
65
+ const PYTHON_TRACEBACK_HEADER: RegExp = /^Traceback \(most recent call last\):/;
66
+ const JAVA_THREAD_PREFIX: RegExp = /^Exception in thread\s+"[^"]*"\s+(.*)$/;
67
+ const GO_PANIC: RegExp = /^panic:\s*(.*)$/;
68
+ /*
69
+ * Leading identifier is optional so a bare "Error: msg" / "Exception: msg"
70
+ * (common in Node.js) matches as well as "TypeError" / "java.lang.IOException".
71
+ */
72
+ const TYPED_ERROR: RegExp =
73
+ /^((?:[A-Za-z_][\w.$]*)?(?:Error|Exception|Warning|Fault))(?::\s*([\s\S]*))?$/;
74
+ const QUALIFIED_TYPE: RegExp = /^([A-Za-z_][\w.$]*\.[A-Za-z_][\w.$]*):\s*(.*)$/;
75
+
76
+ export default class LogExceptionExtractor {
77
+ /**
78
+ * Detect an exception in a single log record. Returns null when none is
79
+ * found. Never throws — extraction must never fail log ingest.
80
+ */
81
+ public static extractFromLogRecord(
82
+ input: LogExceptionExtractorInput,
83
+ ): ExtractedLogException | null {
84
+ try {
85
+ /*
86
+ * Path A — explicit OTel exception.* attributes. Always on, cheapest,
87
+ * highest-signal (the app explicitly recorded an exception on the log).
88
+ */
89
+ const fromAttributes: ExtractedLogException | null =
90
+ LogExceptionExtractor.extractFromAttributes(input.attributes);
91
+ if (fromAttributes) {
92
+ return fromAttributes;
93
+ }
94
+
95
+ // Path B — raw body scan. Gated to keep the hot path cheap.
96
+ if (input.severityNumber < MIN_ERROR_SEVERITY_NUMBER) {
97
+ return null;
98
+ }
99
+ if (input.hasTraceAndSpan) {
100
+ return null;
101
+ }
102
+ return LogExceptionExtractor.extractFromBody(input.body);
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ private static extractFromAttributes(
109
+ attributes: JSONObject,
110
+ ): ExtractedLogException | null {
111
+ if (!attributes) {
112
+ return null;
113
+ }
114
+
115
+ const stackTrace: string = asString(attributes["exception.stacktrace"]);
116
+ const exceptionType: string = asString(attributes["exception.type"]);
117
+ const message: string = asString(attributes["exception.message"]);
118
+
119
+ if (!stackTrace && !exceptionType && !message) {
120
+ return null;
121
+ }
122
+
123
+ const clampedStack: string = clamp(
124
+ stackTrace,
125
+ MAX_STORED_STACK_TRACE_LENGTH,
126
+ );
127
+
128
+ let parsedFrames: string = "[]";
129
+ if (clampedStack) {
130
+ try {
131
+ const parsed: ParsedStackTrace = StackTraceParser.parse(clampedStack);
132
+ parsedFrames = JSON.stringify(parsed.frames);
133
+ } catch {
134
+ parsedFrames = "[]";
135
+ }
136
+ }
137
+
138
+ return {
139
+ message: clamp(message, MAX_MESSAGE_LENGTH),
140
+ exceptionType: exceptionType,
141
+ stackTrace: clampedStack,
142
+ parsedFrames: parsedFrames,
143
+ escaped: toNullableBoolean(attributes["exception.escaped"]),
144
+ };
145
+ }
146
+
147
+ private static extractFromBody(body: string): ExtractedLogException | null {
148
+ if (!body) {
149
+ return null;
150
+ }
151
+
152
+ const sliced: string =
153
+ body.length > MAX_PARSE_BODY_LENGTH
154
+ ? body.slice(0, MAX_PARSE_BODY_LENGTH)
155
+ : body;
156
+
157
+ if (!LOOKS_LIKE_STACK_TRACE.test(sliced)) {
158
+ return null;
159
+ }
160
+
161
+ const parsed: ParsedStackTrace = StackTraceParser.parse(sliced);
162
+
163
+ /*
164
+ * Require at least one parsed frame. A signature match with zero frames is
165
+ * usually prose that merely mentions "...Error:" / "...Exception" — not an
166
+ * actual stack trace. Path A is the path allowed to emit without frames.
167
+ */
168
+ if (!parsed.frames || parsed.frames.length === 0) {
169
+ return null;
170
+ }
171
+
172
+ const header: { exceptionType: string; message: string } =
173
+ LogExceptionExtractor.parseHeader(sliced);
174
+
175
+ return {
176
+ message: clamp(header.message, MAX_MESSAGE_LENGTH),
177
+ exceptionType: header.exceptionType,
178
+ stackTrace: clamp(sliced, MAX_STORED_STACK_TRACE_LENGTH),
179
+ parsedFrames: JSON.stringify(parsed.frames),
180
+ escaped: null,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Best-effort derivation of exceptionType + message from a raw stack-trace
186
+ * body. A stable, clean exceptionType improves grouping and exception-monitor
187
+ * targeting, but a miss is harmless — the fingerprint also uses the message
188
+ * and the normalized stack trace.
189
+ */
190
+ private static parseHeader(body: string): {
191
+ exceptionType: string;
192
+ message: string;
193
+ } {
194
+ const lines: Array<string> = body
195
+ .split("\n")
196
+ .map((l: string) => {
197
+ return l.trim();
198
+ })
199
+ .filter((l: string) => {
200
+ return l.length > 0;
201
+ });
202
+
203
+ if (lines.length === 0) {
204
+ return { exceptionType: "", message: "" };
205
+ }
206
+
207
+ /*
208
+ * Python: header is "Traceback (most recent call last):"; the "Type: message"
209
+ * line is at the BOTTOM of the traceback, so scan upward for it.
210
+ */
211
+ if (PYTHON_TRACEBACK_HEADER.test(lines[0]!)) {
212
+ for (let i: number = lines.length - 1; i >= 0; i--) {
213
+ const candidate: RegExpMatchArray | null = lines[i]!.match(TYPED_ERROR);
214
+ if (candidate) {
215
+ return {
216
+ exceptionType: candidate[1] || "",
217
+ message: (candidate[2] || "").trim(),
218
+ };
219
+ }
220
+ }
221
+ // Truncated traceback with no type line — keep the last line as message.
222
+ return { exceptionType: "", message: lines[lines.length - 1]! };
223
+ }
224
+
225
+ // Strip Java's "Exception in thread "name"" prefix if present.
226
+ let firstLine: string = lines[0]!;
227
+ const threadMatch: RegExpMatchArray | null =
228
+ firstLine.match(JAVA_THREAD_PREFIX);
229
+ if (threadMatch) {
230
+ firstLine = threadMatch[1]!.trim();
231
+ }
232
+
233
+ const goMatch: RegExpMatchArray | null = firstLine.match(GO_PANIC);
234
+ if (goMatch) {
235
+ return { exceptionType: "panic", message: (goMatch[1] || "").trim() };
236
+ }
237
+
238
+ // "TypeError: msg", "java.lang.NullPointerException: msg", "...Exception"
239
+ const typedMatch: RegExpMatchArray | null = firstLine.match(TYPED_ERROR);
240
+ if (typedMatch) {
241
+ return {
242
+ exceptionType: typedMatch[1] || "",
243
+ message: (typedMatch[2] || "").trim(),
244
+ };
245
+ }
246
+
247
+ // Generic qualified "pkg.Sub.Type: msg"
248
+ const qualifiedMatch: RegExpMatchArray | null =
249
+ firstLine.match(QUALIFIED_TYPE);
250
+ if (qualifiedMatch) {
251
+ return {
252
+ exceptionType: qualifiedMatch[1] || "",
253
+ message: (qualifiedMatch[2] || "").trim(),
254
+ };
255
+ }
256
+
257
+ return { exceptionType: "", message: firstLine };
258
+ }
259
+ }
260
+
261
+ function asString(value: JSONValue | undefined): string {
262
+ if (typeof value === "string") {
263
+ return value;
264
+ }
265
+ return "";
266
+ }
267
+
268
+ function toNullableBoolean(value: JSONValue | undefined): boolean | null {
269
+ if (typeof value === "boolean") {
270
+ return value;
271
+ }
272
+ if (typeof value === "string") {
273
+ const normalized: string = value.trim().toLowerCase();
274
+ if (normalized === "true") {
275
+ return true;
276
+ }
277
+ if (normalized === "false") {
278
+ return false;
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+
284
+ function clamp(value: string, max: number): string {
285
+ if (value && value.length > max) {
286
+ return value.slice(0, max);
287
+ }
288
+ return value;
289
+ }
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Stack trace parser that transforms raw stack trace strings into structured frames.
3
+ * Supports JavaScript/Node.js, Python, Java, Go, Ruby, C#/.NET, and PHP stack traces.
4
+ */
5
+
6
+ export interface StackFrame {
7
+ functionName: string;
8
+ fileName: string;
9
+ lineNumber: number;
10
+ columnNumber?: number;
11
+ inApp: boolean; // true if user code, false if library/framework code
12
+ }
13
+
14
+ export interface ParsedStackTrace {
15
+ frames: StackFrame[];
16
+ raw: string; // original raw stack trace string
17
+ }
18
+
19
+ // Known library/framework path patterns that indicate non-app code
20
+ const LIBRARY_PATTERNS: Array<RegExp> = [
21
+ // Node.js internals
22
+ /^node:/,
23
+ /^internal\//,
24
+ /node_modules\//,
25
+ /^events\.js$/,
26
+ /^timers\.js$/,
27
+ /^util\.js$/,
28
+ /^net\.js$/,
29
+ /^stream\.js$/,
30
+ /^buffer\.js$/,
31
+ // Python
32
+ /\/site-packages\//,
33
+ /\/dist-packages\//,
34
+ /\/lib\/python\d+\.\d+\//,
35
+ /\/usr\/lib\//,
36
+ /\/usr\/local\/lib\//,
37
+ /\/venv\//,
38
+ /\/\.venv\//,
39
+ /\/virtualenv\//,
40
+ // Java
41
+ /^java\./,
42
+ /^javax\./,
43
+ /^sun\./,
44
+ /^com\.sun\./,
45
+ /^org\.springframework\./,
46
+ /^org\.apache\./,
47
+ /^org\.hibernate\./,
48
+ /^org\.eclipse\./,
49
+ /^io\.netty\./,
50
+ /^com\.google\./,
51
+ /^org\.junit\./,
52
+ // Go
53
+ /^runtime\//,
54
+ /^net\/http\//,
55
+ /^testing\//,
56
+ /\/vendor\//,
57
+ /\/pkg\/mod\//,
58
+ // Ruby
59
+ /\/gems\//,
60
+ /\/rubygems\//,
61
+ /\/ruby\/\d+\.\d+\.\d+\//,
62
+ // C#/.NET
63
+ /^System\./,
64
+ /^Microsoft\./,
65
+ /^Newtonsoft\./,
66
+ // PHP
67
+ /\/vendor\//,
68
+ /^phar:\/\//,
69
+ ];
70
+
71
+ export default class StackTraceParser {
72
+ /**
73
+ * Parse a raw stack trace string into structured frames.
74
+ * Auto-detects the language and applies the appropriate parser.
75
+ */
76
+ public static parse(rawStackTrace: string): ParsedStackTrace {
77
+ if (!rawStackTrace || rawStackTrace.trim().length === 0) {
78
+ return { frames: [], raw: rawStackTrace || "" };
79
+ }
80
+
81
+ const lines: string[] = rawStackTrace.split("\n").map((l: string) => {
82
+ return l.trim();
83
+ });
84
+
85
+ // Try each parser and use the one that produces the most frames
86
+ const parsers: Array<(lines: string[]) => StackFrame[]> = [
87
+ StackTraceParser.parseJavaScript,
88
+ StackTraceParser.parsePython,
89
+ StackTraceParser.parseJava,
90
+ StackTraceParser.parseGo,
91
+ StackTraceParser.parseRuby,
92
+ StackTraceParser.parseCSharp,
93
+ StackTraceParser.parsePHP,
94
+ ];
95
+
96
+ let bestFrames: StackFrame[] = [];
97
+
98
+ for (const parser of parsers) {
99
+ try {
100
+ const frames: StackFrame[] = parser(lines);
101
+ if (frames.length > bestFrames.length) {
102
+ bestFrames = frames;
103
+ }
104
+ } catch {
105
+ // Skip failing parsers
106
+ }
107
+ }
108
+
109
+ return {
110
+ frames: bestFrames,
111
+ raw: rawStackTrace,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Determine if a file path is application code (not library/framework).
117
+ */
118
+ private static isAppCode(filePath: string): boolean {
119
+ if (!filePath) {
120
+ return true;
121
+ }
122
+
123
+ for (const pattern of LIBRARY_PATTERNS) {
124
+ if (pattern.test(filePath)) {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Parse JavaScript/Node.js stack traces.
134
+ * Format: `at functionName (filePath:line:col)` or `at filePath:line:col`
135
+ */
136
+ private static parseJavaScript(lines: string[]): StackFrame[] {
137
+ const frames: StackFrame[] = [];
138
+
139
+ // Pattern 1: at functionName (filePath:line:col)
140
+ const patternWithParens: RegExp = /^at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)$/;
141
+ // Pattern 2: at filePath:line:col
142
+ const patternWithoutParens: RegExp = /^at\s+(.+?):(\d+):(\d+)$/;
143
+ // Pattern 3: at functionName (filePath:line)
144
+ const patternWithParensNoCol: RegExp = /^at\s+(.+?)\s+\((.+?):(\d+)\)$/;
145
+ // Pattern 4: at eval (eval at functionName (filePath:line:col))
146
+ const patternEval: RegExp =
147
+ /^at\s+eval\s+\(eval\s+at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/;
148
+
149
+ for (const line of lines) {
150
+ let match: RegExpMatchArray | null = null;
151
+
152
+ match = line.match(patternEval);
153
+ if (match) {
154
+ frames.push({
155
+ functionName: `eval at ${match[1]!}`,
156
+ fileName: match[2]!,
157
+ lineNumber: parseInt(match[3]!, 10),
158
+ columnNumber: parseInt(match[4]!, 10),
159
+ inApp: StackTraceParser.isAppCode(match[2]!),
160
+ });
161
+ continue;
162
+ }
163
+
164
+ match = line.match(patternWithParens);
165
+ if (match) {
166
+ frames.push({
167
+ functionName: match[1]!,
168
+ fileName: match[2]!,
169
+ lineNumber: parseInt(match[3]!, 10),
170
+ columnNumber: parseInt(match[4]!, 10),
171
+ inApp: StackTraceParser.isAppCode(match[2]!),
172
+ });
173
+ continue;
174
+ }
175
+
176
+ match = line.match(patternWithParensNoCol);
177
+ if (match) {
178
+ frames.push({
179
+ functionName: match[1]!,
180
+ fileName: match[2]!,
181
+ lineNumber: parseInt(match[3]!, 10),
182
+ inApp: StackTraceParser.isAppCode(match[2]!),
183
+ });
184
+ continue;
185
+ }
186
+
187
+ match = line.match(patternWithoutParens);
188
+ if (match) {
189
+ frames.push({
190
+ functionName: "<anonymous>",
191
+ fileName: match[1]!,
192
+ lineNumber: parseInt(match[2]!, 10),
193
+ columnNumber: parseInt(match[3]!, 10),
194
+ inApp: StackTraceParser.isAppCode(match[1]!),
195
+ });
196
+ continue;
197
+ }
198
+ }
199
+
200
+ return frames;
201
+ }
202
+
203
+ /**
204
+ * Parse Python stack traces.
205
+ * Format: `File "path", line N, in function`
206
+ */
207
+ private static parsePython(lines: string[]): StackFrame[] {
208
+ const frames: StackFrame[] = [];
209
+ const pattern: RegExp =
210
+ /^File\s+"(.+?)",\s+line\s+(\d+)(?:,\s+in\s+(.+))?$/;
211
+
212
+ for (const line of lines) {
213
+ const match: RegExpMatchArray | null = line.match(pattern);
214
+ if (match) {
215
+ frames.push({
216
+ functionName: match[3] || "<module>",
217
+ fileName: match[1]!,
218
+ lineNumber: parseInt(match[2]!, 10),
219
+ inApp: StackTraceParser.isAppCode(match[1]!),
220
+ });
221
+ }
222
+ }
223
+
224
+ return frames;
225
+ }
226
+
227
+ /**
228
+ * Parse Java stack traces.
229
+ * Format: `at package.Class.method(File.java:line)`
230
+ */
231
+ private static parseJava(lines: string[]): StackFrame[] {
232
+ const frames: StackFrame[] = [];
233
+ // Pattern: at com.package.Class.method(File.java:123)
234
+ const pattern: RegExp = /^at\s+([\w.$]+)\(([\w.]+):(\d+)\)$/;
235
+ // Pattern for native methods: at com.package.Class.method(Native Method)
236
+ const patternNative: RegExp = /^at\s+([\w.$]+)\(Native Method\)$/;
237
+ // Pattern for unknown source: at com.package.Class.method(Unknown Source)
238
+ const patternUnknown: RegExp = /^at\s+([\w.$]+)\(Unknown Source\)$/;
239
+
240
+ for (const line of lines) {
241
+ let match: RegExpMatchArray | null = null;
242
+
243
+ match = line.match(pattern);
244
+ if (match) {
245
+ const fullMethod: string = match[1]!;
246
+ frames.push({
247
+ functionName: fullMethod,
248
+ fileName: match[2]!,
249
+ lineNumber: parseInt(match[3]!, 10),
250
+ inApp: StackTraceParser.isAppCode(fullMethod),
251
+ });
252
+ continue;
253
+ }
254
+
255
+ match = line.match(patternNative);
256
+ if (match) {
257
+ frames.push({
258
+ functionName: match[1]!,
259
+ fileName: "Native Method",
260
+ lineNumber: 0,
261
+ inApp: false,
262
+ });
263
+ continue;
264
+ }
265
+
266
+ match = line.match(patternUnknown);
267
+ if (match) {
268
+ frames.push({
269
+ functionName: match[1]!,
270
+ fileName: "Unknown Source",
271
+ lineNumber: 0,
272
+ inApp: StackTraceParser.isAppCode(match[1]!),
273
+ });
274
+ continue;
275
+ }
276
+ }
277
+
278
+ return frames;
279
+ }
280
+
281
+ /**
282
+ * Parse Go stack traces.
283
+ * Format: `package/file.go:line +0xNN` or `goroutine N [reason]:`
284
+ * Go stack traces have pairs of lines:
285
+ * functionName(args)
286
+ * /path/to/file.go:line +0xNN
287
+ */
288
+ private static parseGo(lines: string[]): StackFrame[] {
289
+ const frames: StackFrame[] = [];
290
+ const filePattern: RegExp = /^(.+\.go):(\d+)\s*(?:\+0x[0-9a-f]+)?$/;
291
+
292
+ for (let i: number = 0; i < lines.length; i++) {
293
+ const line: string = lines[i]!;
294
+
295
+ // Skip goroutine headers
296
+ if (line.startsWith("goroutine ")) {
297
+ continue;
298
+ }
299
+
300
+ // Look for file:line pattern
301
+ const match: RegExpMatchArray | null = line.match(filePattern);
302
+ if (match) {
303
+ // The previous line should be the function name
304
+ let functionName: string = "<unknown>";
305
+ if (i > 0 && lines[i - 1]) {
306
+ // Remove arguments from function name
307
+ const funcLine: string = lines[i - 1]!;
308
+ const parenIndex: number = funcLine.indexOf("(");
309
+ functionName =
310
+ parenIndex > 0 ? funcLine.substring(0, parenIndex) : funcLine;
311
+ }
312
+
313
+ frames.push({
314
+ functionName: functionName,
315
+ fileName: match[1]!,
316
+ lineNumber: parseInt(match[2]!, 10),
317
+ inApp: StackTraceParser.isAppCode(match[1]!),
318
+ });
319
+ }
320
+ }
321
+
322
+ return frames;
323
+ }
324
+
325
+ /**
326
+ * Parse Ruby stack traces.
327
+ * Format: `file:line:in 'method'` or `file:line:in \`method'`
328
+ */
329
+ private static parseRuby(lines: string[]): StackFrame[] {
330
+ const frames: StackFrame[] = [];
331
+ const pattern: RegExp = /^(.+?):(\d+):in\s+[`'](.+?)'$/;
332
+
333
+ for (const line of lines) {
334
+ const match: RegExpMatchArray | null = line.match(pattern);
335
+ if (match) {
336
+ frames.push({
337
+ functionName: match[3]!,
338
+ fileName: match[1]!,
339
+ lineNumber: parseInt(match[2]!, 10),
340
+ inApp: StackTraceParser.isAppCode(match[1]!),
341
+ });
342
+ }
343
+ }
344
+
345
+ return frames;
346
+ }
347
+
348
+ /**
349
+ * Parse C#/.NET stack traces.
350
+ * Format: `at Namespace.Class.Method(params) in file:line N`
351
+ */
352
+ private static parseCSharp(lines: string[]): StackFrame[] {
353
+ const frames: StackFrame[] = [];
354
+ // Pattern: at Namespace.Class.Method(params) in /path/to/file.cs:line 42
355
+ const patternWithFile: RegExp = /^at\s+(.+?)\s+in\s+(.+?):line\s+(\d+)$/;
356
+ // Pattern: at Namespace.Class.Method(params)
357
+ const patternWithoutFile: RegExp = /^at\s+([\w.<>+]+\(.*?\))$/;
358
+
359
+ for (const line of lines) {
360
+ let match: RegExpMatchArray | null = null;
361
+
362
+ match = line.match(patternWithFile);
363
+ if (match) {
364
+ frames.push({
365
+ functionName: match[1]!,
366
+ fileName: match[2]!,
367
+ lineNumber: parseInt(match[3]!, 10),
368
+ inApp: StackTraceParser.isAppCode(match[1]!),
369
+ });
370
+ continue;
371
+ }
372
+
373
+ match = line.match(patternWithoutFile);
374
+ if (match) {
375
+ frames.push({
376
+ functionName: match[1]!,
377
+ fileName: "",
378
+ lineNumber: 0,
379
+ inApp: StackTraceParser.isAppCode(match[1]!),
380
+ });
381
+ continue;
382
+ }
383
+ }
384
+
385
+ return frames;
386
+ }
387
+
388
+ /**
389
+ * Parse PHP stack traces.
390
+ * Format: `#N /path/to/file.php(line): Class->method()`
391
+ */
392
+ private static parsePHP(lines: string[]): StackFrame[] {
393
+ const frames: StackFrame[] = [];
394
+ // Pattern: #0 /path/to/file.php(42): ClassName->method()
395
+ const pattern: RegExp = /^#\d+\s+(.+?)\((\d+)\):\s+(.+)$/;
396
+ // Pattern: #0 {main}
397
+ const patternMain: RegExp = /^#\d+\s+\{main\}$/;
398
+
399
+ for (const line of lines) {
400
+ if (patternMain.test(line)) {
401
+ frames.push({
402
+ functionName: "{main}",
403
+ fileName: "",
404
+ lineNumber: 0,
405
+ inApp: true,
406
+ });
407
+ continue;
408
+ }
409
+
410
+ const match: RegExpMatchArray | null = line.match(pattern);
411
+ if (match) {
412
+ frames.push({
413
+ functionName: match[3]!.replace(/\(\)$/, ""),
414
+ fileName: match[1]!,
415
+ lineNumber: parseInt(match[2]!, 10),
416
+ inApp: StackTraceParser.isAppCode(match[1]!),
417
+ });
418
+ }
419
+ }
420
+
421
+ return frames;
422
+ }
423
+ }