@descryy/runtime-adapter-ruby 0.0.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/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/rails-frame-parsing.d.ts +105 -0
- package/dist/rails-frame-parsing.d.ts.map +1 -0
- package/dist/rails-frame-parsing.js +183 -0
- package/dist/rails-frame-parsing.js.map +1 -0
- package/dist/rails-stack-trace-parser.d.ts +31 -0
- package/dist/rails-stack-trace-parser.d.ts.map +1 -0
- package/dist/rails-stack-trace-parser.js +63 -0
- package/dist/rails-stack-trace-parser.js.map +1 -0
- package/dist/ruby-frame-parsing.d.ts +78 -0
- package/dist/ruby-frame-parsing.d.ts.map +1 -0
- package/dist/ruby-frame-parsing.js +108 -0
- package/dist/ruby-frame-parsing.js.map +1 -0
- package/dist/ruby-log-envelopes.d.ts +101 -0
- package/dist/ruby-log-envelopes.d.ts.map +1 -0
- package/dist/ruby-log-envelopes.js +214 -0
- package/dist/ruby-log-envelopes.js.map +1 -0
- package/dist/ruby-runtime-adapter.d.ts +51 -0
- package/dist/ruby-runtime-adapter.d.ts.map +1 -0
- package/dist/ruby-runtime-adapter.js +56 -0
- package/dist/ruby-runtime-adapter.js.map +1 -0
- package/dist/ruby-source-location-resolver.d.ts +34 -0
- package/dist/ruby-source-location-resolver.d.ts.map +1 -0
- package/dist/ruby-source-location-resolver.js +61 -0
- package/dist/ruby-source-location-resolver.js.map +1 -0
- package/dist/ruby-stack-trace-parser.d.ts +24 -0
- package/dist/ruby-stack-trace-parser.d.ts.map +1 -0
- package/dist/ruby-stack-trace-parser.js +71 -0
- package/dist/ruby-stack-trace-parser.js.map +1 -0
- package/dist/sinatra-frame-parsing.d.ts +92 -0
- package/dist/sinatra-frame-parsing.d.ts.map +1 -0
- package/dist/sinatra-frame-parsing.js +142 -0
- package/dist/sinatra-frame-parsing.js.map +1 -0
- package/dist/sinatra-stack-trace-parser.d.ts +18 -0
- package/dist/sinatra-stack-trace-parser.d.ts.map +1 -0
- package/dist/sinatra-stack-trace-parser.js +50 -0
- package/dist/sinatra-stack-trace-parser.js.map +1 -0
- package/package.json +30 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { RawRailsFrame } from "./rails-frame-parsing.ts";
|
|
2
|
+
export { createRailsLineClassifier, isRailsHeaderLine, isRailsFrameLine, parseRailsHeader, parseRailsFrame, } from "./rails-frame-parsing.ts";
|
|
3
|
+
export { createRailsStackTraceParser } from "./rails-stack-trace-parser.ts";
|
|
4
|
+
export type { RawSinatraFrame } from "./sinatra-frame-parsing.ts";
|
|
5
|
+
export { createSinatraLineClassifier, isSinatraHeaderLine, isSinatraFrameLine, parseSinatraHeader, parseSinatraFrame, } from "./sinatra-frame-parsing.ts";
|
|
6
|
+
export { createSinatraStackTraceParser } from "./sinatra-stack-trace-parser.ts";
|
|
7
|
+
export type { RawRubyFrame } from "./ruby-frame-parsing.ts";
|
|
8
|
+
export { createRubyLineClassifier, isRubyHeaderLine, isRubyFrameLine, parseRubyHeaderFrame, parseRubyFromFrame, } from "./ruby-frame-parsing.ts";
|
|
9
|
+
export { createRubySourceLocationResolver } from "./ruby-source-location-resolver.ts";
|
|
10
|
+
export { createRubyStackTraceParser } from "./ruby-stack-trace-parser.ts";
|
|
11
|
+
export { createRubyLoggerEnvelope, createRailsTaggedLogEnvelope } from "./ruby-log-envelopes.ts";
|
|
12
|
+
export type { RubyRuntimeAdapterOptions } from "./ruby-runtime-adapter.ts";
|
|
13
|
+
export { createRubyRuntimeAdapter } from "./ruby-runtime-adapter.ts";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AACjG,YAAY,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createRailsLineClassifier, isRailsHeaderLine, isRailsFrameLine, parseRailsHeader, parseRailsFrame, } from "./rails-frame-parsing.js";
|
|
2
|
+
export { createRailsStackTraceParser } from "./rails-stack-trace-parser.js";
|
|
3
|
+
export { createSinatraLineClassifier, isSinatraHeaderLine, isSinatraFrameLine, parseSinatraHeader, parseSinatraFrame, } from "./sinatra-frame-parsing.js";
|
|
4
|
+
export { createSinatraStackTraceParser } from "./sinatra-stack-trace-parser.js";
|
|
5
|
+
export { createRubyLineClassifier, isRubyHeaderLine, isRubyFrameLine, parseRubyHeaderFrame, parseRubyFromFrame, } from "./ruby-frame-parsing.js";
|
|
6
|
+
export { createRubySourceLocationResolver } from "./ruby-source-location-resolver.js";
|
|
7
|
+
export { createRubyStackTraceParser } from "./ruby-stack-trace-parser.js";
|
|
8
|
+
export { createRubyLoggerEnvelope, createRailsTaggedLogEnvelope } from "./ruby-log-envelopes.js";
|
|
9
|
+
export { createRubyRuntimeAdapter } from "./ruby-runtime-adapter.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAE5E,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAEhF,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AAEjG,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rails' backtrace grammar, which is **not Ruby's** (RT-111).
|
|
3
|
+
*
|
|
4
|
+
* ## The measurement
|
|
5
|
+
*
|
|
6
|
+
* A real Rails 8.0.4 application, spawned through the managed lifecycle,
|
|
7
|
+
* failing through its own request pipeline, logging through
|
|
8
|
+
* `ActiveSupport::TaggedLogging`, produced:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* 77 lines in -> 0 exceptions, 0 stack traces, 77 plain logs
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Zero. With the Rails log envelope applied and without it, identically.
|
|
15
|
+
* RT-089's shape for the third time (RT-089 ASP.NET, RT-104 PEP 654) and the
|
|
16
|
+
* most consequential of the three, because Rails is *the* Ruby framework and
|
|
17
|
+
* this is every exception a Rails application reports.
|
|
18
|
+
*
|
|
19
|
+
* ## Why the Ruby classifier cannot see it
|
|
20
|
+
*
|
|
21
|
+
* `ActionDispatch::DebugExceptions` does not log `Exception#full_message`.
|
|
22
|
+
* It composes its own text, and both halves differ:
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* NoMethodError (undefined method 'fetch' for nil): <- Class (message):
|
|
26
|
+
*
|
|
27
|
+
* rails_service.rb:69:in 'ProbeController#read_customer_name'
|
|
28
|
+
* rails_service.rb:73:in 'ProbeController#charge_invoice'
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* against Ruby's own printer:
|
|
32
|
+
*
|
|
33
|
+
* ```
|
|
34
|
+
* rails_service.rb:69:in 'ProbeController#read_customer_name': undefined method 'fetch' for nil (NoMethodError)
|
|
35
|
+
* from rails_service.rb:73:in 'ProbeController#charge_invoice'
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* - **The header carries no location.** Ruby's header IS the innermost
|
|
39
|
+
* frame; Rails' names only the class and the message, and the raise site
|
|
40
|
+
* moves down to the first backtrace line.
|
|
41
|
+
* - **The frames have no `from` prefix.** `FROM_FRAME_PATTERN` requires one,
|
|
42
|
+
* and `HEADER_PATTERN` requires a trailing `: message` that these lines do
|
|
43
|
+
* not have. So a Rails backtrace line matches neither, and the block never
|
|
44
|
+
* opens.
|
|
45
|
+
*
|
|
46
|
+
* ## The fix is a declared framework, not a looser Ruby pattern
|
|
47
|
+
*
|
|
48
|
+
* Widening Ruby's `isFrame` to accept a bare `file:line:in 'name'` was the
|
|
49
|
+
* cheaper change and is the wrong one. `ExceptionBlockDetector` opens a
|
|
50
|
+
* block the moment `isFrame` returns true from idle, so that pattern would
|
|
51
|
+
* turn **any** log line containing `something.rb:12:in 'foo'` into the start
|
|
52
|
+
* of a stack trace — a wrong edge, which rule 2 ranks as worse than a
|
|
53
|
+
* missing one because it corrupts everything above it.
|
|
54
|
+
*
|
|
55
|
+
* So this is a second classifier, selected by
|
|
56
|
+
* `createRubyRuntimeAdapter({ framework: "rails" })` — **declared, never
|
|
57
|
+
* sniffed**, the same rule `createCSharpRuntimeAdapter`'s `framework` option
|
|
58
|
+
* follows and for the same reason: detecting Rails from a line that looks
|
|
59
|
+
* like its output would apply this grammar to a plain Ruby process that
|
|
60
|
+
* happened to print one.
|
|
61
|
+
*
|
|
62
|
+
* ## The bare-frame pattern is still loose, and is confined rather than tightened
|
|
63
|
+
*
|
|
64
|
+
* `isFrame` here is stateful: a bare `file:line:in 'name'` is only a frame
|
|
65
|
+
* **after a header has been seen**, which is the same argument
|
|
66
|
+
* `createPythonLineClassifier` makes about indented lines. Out of that
|
|
67
|
+
* state the pattern is never consulted from idle, so it cannot open a block
|
|
68
|
+
* on its own.
|
|
69
|
+
*
|
|
70
|
+
* ## The blank line between the header and the first frame is a bridge
|
|
71
|
+
*
|
|
72
|
+
* Rails prints a whitespace-only line after the header (and another before
|
|
73
|
+
* it). `LineClassifier.isBridge` exists for exactly this — PHP's `Stack
|
|
74
|
+
* trace:` line was the first — and `ExceptionBlockDetector` consults it only
|
|
75
|
+
* while a header is already held, so a blank line elsewhere in the log opens
|
|
76
|
+
* nothing.
|
|
77
|
+
*/
|
|
78
|
+
export interface RawRailsFrame {
|
|
79
|
+
/** `Class#method`, `Class.method`, `block in Class#method` — kept verbatim, never rewritten. */
|
|
80
|
+
readonly functionName: string | null;
|
|
81
|
+
readonly file: string;
|
|
82
|
+
readonly line: number;
|
|
83
|
+
}
|
|
84
|
+
export declare function isRailsHeaderLine(line: string): boolean;
|
|
85
|
+
export declare function isRailsFrameLine(line: string): boolean;
|
|
86
|
+
/** `null` when the line is not `Class (message):` shaped — never fabricated. */
|
|
87
|
+
export declare function parseRailsHeader(line: string): {
|
|
88
|
+
readonly exceptionClass: string;
|
|
89
|
+
readonly message: string;
|
|
90
|
+
} | null;
|
|
91
|
+
/** `null` for anything not shaped like a bare `file:line:in 'name'` frame — never fabricated. */
|
|
92
|
+
export declare function parseRailsFrame(line: string): RawRailsFrame | null;
|
|
93
|
+
/**
|
|
94
|
+
* A stateful `LineClassifier`, and the statefulness is what makes the loose
|
|
95
|
+
* frame pattern safe. See this file's header.
|
|
96
|
+
*
|
|
97
|
+
* A fresh classifier per collector keeps two processes' output from priming
|
|
98
|
+
* each other — the same construction rule every other adapter here follows.
|
|
99
|
+
*/
|
|
100
|
+
export declare function createRailsLineClassifier(): {
|
|
101
|
+
isHeader(line: string): boolean;
|
|
102
|
+
isFrame(line: string): boolean;
|
|
103
|
+
isBridge(line: string): boolean;
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=rails-frame-parsing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rails-frame-parsing.d.ts","sourceRoot":"","sources":["../src/rails-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AAEH,MAAM,WAAW,aAAa;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAmCD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAMnH;AAED,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAOlE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,IAAI;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAuCA"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rails' backtrace grammar, which is **not Ruby's** (RT-111).
|
|
3
|
+
*
|
|
4
|
+
* ## The measurement
|
|
5
|
+
*
|
|
6
|
+
* A real Rails 8.0.4 application, spawned through the managed lifecycle,
|
|
7
|
+
* failing through its own request pipeline, logging through
|
|
8
|
+
* `ActiveSupport::TaggedLogging`, produced:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* 77 lines in -> 0 exceptions, 0 stack traces, 77 plain logs
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Zero. With the Rails log envelope applied and without it, identically.
|
|
15
|
+
* RT-089's shape for the third time (RT-089 ASP.NET, RT-104 PEP 654) and the
|
|
16
|
+
* most consequential of the three, because Rails is *the* Ruby framework and
|
|
17
|
+
* this is every exception a Rails application reports.
|
|
18
|
+
*
|
|
19
|
+
* ## Why the Ruby classifier cannot see it
|
|
20
|
+
*
|
|
21
|
+
* `ActionDispatch::DebugExceptions` does not log `Exception#full_message`.
|
|
22
|
+
* It composes its own text, and both halves differ:
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* NoMethodError (undefined method 'fetch' for nil): <- Class (message):
|
|
26
|
+
*
|
|
27
|
+
* rails_service.rb:69:in 'ProbeController#read_customer_name'
|
|
28
|
+
* rails_service.rb:73:in 'ProbeController#charge_invoice'
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* against Ruby's own printer:
|
|
32
|
+
*
|
|
33
|
+
* ```
|
|
34
|
+
* rails_service.rb:69:in 'ProbeController#read_customer_name': undefined method 'fetch' for nil (NoMethodError)
|
|
35
|
+
* from rails_service.rb:73:in 'ProbeController#charge_invoice'
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* - **The header carries no location.** Ruby's header IS the innermost
|
|
39
|
+
* frame; Rails' names only the class and the message, and the raise site
|
|
40
|
+
* moves down to the first backtrace line.
|
|
41
|
+
* - **The frames have no `from` prefix.** `FROM_FRAME_PATTERN` requires one,
|
|
42
|
+
* and `HEADER_PATTERN` requires a trailing `: message` that these lines do
|
|
43
|
+
* not have. So a Rails backtrace line matches neither, and the block never
|
|
44
|
+
* opens.
|
|
45
|
+
*
|
|
46
|
+
* ## The fix is a declared framework, not a looser Ruby pattern
|
|
47
|
+
*
|
|
48
|
+
* Widening Ruby's `isFrame` to accept a bare `file:line:in 'name'` was the
|
|
49
|
+
* cheaper change and is the wrong one. `ExceptionBlockDetector` opens a
|
|
50
|
+
* block the moment `isFrame` returns true from idle, so that pattern would
|
|
51
|
+
* turn **any** log line containing `something.rb:12:in 'foo'` into the start
|
|
52
|
+
* of a stack trace — a wrong edge, which rule 2 ranks as worse than a
|
|
53
|
+
* missing one because it corrupts everything above it.
|
|
54
|
+
*
|
|
55
|
+
* So this is a second classifier, selected by
|
|
56
|
+
* `createRubyRuntimeAdapter({ framework: "rails" })` — **declared, never
|
|
57
|
+
* sniffed**, the same rule `createCSharpRuntimeAdapter`'s `framework` option
|
|
58
|
+
* follows and for the same reason: detecting Rails from a line that looks
|
|
59
|
+
* like its output would apply this grammar to a plain Ruby process that
|
|
60
|
+
* happened to print one.
|
|
61
|
+
*
|
|
62
|
+
* ## The bare-frame pattern is still loose, and is confined rather than tightened
|
|
63
|
+
*
|
|
64
|
+
* `isFrame` here is stateful: a bare `file:line:in 'name'` is only a frame
|
|
65
|
+
* **after a header has been seen**, which is the same argument
|
|
66
|
+
* `createPythonLineClassifier` makes about indented lines. Out of that
|
|
67
|
+
* state the pattern is never consulted from idle, so it cannot open a block
|
|
68
|
+
* on its own.
|
|
69
|
+
*
|
|
70
|
+
* ## The blank line between the header and the first frame is a bridge
|
|
71
|
+
*
|
|
72
|
+
* Rails prints a whitespace-only line after the header (and another before
|
|
73
|
+
* it). `LineClassifier.isBridge` exists for exactly this — PHP's `Stack
|
|
74
|
+
* trace:` line was the first — and `ExceptionBlockDetector` consults it only
|
|
75
|
+
* while a header is already held, so a blank line elsewhere in the log opens
|
|
76
|
+
* nothing.
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* `NoMethodError (undefined method 'fetch' for nil):`
|
|
80
|
+
*
|
|
81
|
+
* `ActionDispatch::ExceptionWrapper` composes `"#{class} (#{message}):"`.
|
|
82
|
+
* Anchored at both ends and requiring the trailing `):`, because a partial
|
|
83
|
+
* match is evidence the line is not a header rather than a reason to guess
|
|
84
|
+
* at one. The class group allows `::` so a namespaced
|
|
85
|
+
* `ActiveRecord::RecordNotFound` matches.
|
|
86
|
+
*
|
|
87
|
+
* Not anchored at the *start* of the line beyond leading whitespace: the
|
|
88
|
+
* Rails envelope removes the `[request-id] ` tag run before the classifier
|
|
89
|
+
* sees the line, but an application logging through a different formatter
|
|
90
|
+
* may leave something else there, and Ruby's own header pattern made the
|
|
91
|
+
* same allowance for the same reason.
|
|
92
|
+
*/
|
|
93
|
+
const RAILS_HEADER = /(?<class>[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*) \((?<message>.*)\):\s*$/;
|
|
94
|
+
/**
|
|
95
|
+
* `rails_service.rb:69:in 'ProbeController#read_customer_name'`, or a gem
|
|
96
|
+
* frame `actionpack (8.0.4) lib/action_controller/metal.rb:252:in '...'`.
|
|
97
|
+
*
|
|
98
|
+
* The gem form is real and is why `file` is `.+` rather than a path
|
|
99
|
+
* character class: Rails rewrites a gem's absolute path to
|
|
100
|
+
* `name (version) relative/path`, which is not openable but is the
|
|
101
|
+
* interpreter's — strictly Rails' — own answer, and rewriting it here would
|
|
102
|
+
* invent a location. Same decision as the `eval` frames in RT-105.
|
|
103
|
+
*
|
|
104
|
+
* The closing quote is always `'`; only the opening one varies by Ruby
|
|
105
|
+
* version (Feature #16495), so both are accepted exactly as Ruby's own
|
|
106
|
+
* patterns do.
|
|
107
|
+
*/
|
|
108
|
+
const RAILS_FRAME = /^\s*(?<file>.+):(?<line>\d+):in\s+[`'](?<fn>[^']*)'\s*$/;
|
|
109
|
+
export function isRailsHeaderLine(line) {
|
|
110
|
+
return RAILS_HEADER.test(line);
|
|
111
|
+
}
|
|
112
|
+
export function isRailsFrameLine(line) {
|
|
113
|
+
return RAILS_FRAME.test(line);
|
|
114
|
+
}
|
|
115
|
+
/** `null` when the line is not `Class (message):` shaped — never fabricated. */
|
|
116
|
+
export function parseRailsHeader(line) {
|
|
117
|
+
const match = RAILS_HEADER.exec(line);
|
|
118
|
+
if (!match?.groups)
|
|
119
|
+
return null;
|
|
120
|
+
const exceptionClass = match.groups["class"];
|
|
121
|
+
if (exceptionClass === undefined)
|
|
122
|
+
return null;
|
|
123
|
+
return { exceptionClass, message: match.groups["message"] ?? "" };
|
|
124
|
+
}
|
|
125
|
+
/** `null` for anything not shaped like a bare `file:line:in 'name'` frame — never fabricated. */
|
|
126
|
+
export function parseRailsFrame(line) {
|
|
127
|
+
const match = RAILS_FRAME.exec(line);
|
|
128
|
+
if (!match?.groups)
|
|
129
|
+
return null;
|
|
130
|
+
const file = match.groups["file"];
|
|
131
|
+
const lineNumber = Number(match.groups["line"]);
|
|
132
|
+
if (file === undefined || !Number.isInteger(lineNumber))
|
|
133
|
+
return null;
|
|
134
|
+
return { functionName: match.groups["fn"] ?? null, file, line: lineNumber };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A stateful `LineClassifier`, and the statefulness is what makes the loose
|
|
138
|
+
* frame pattern safe. See this file's header.
|
|
139
|
+
*
|
|
140
|
+
* A fresh classifier per collector keeps two processes' output from priming
|
|
141
|
+
* each other — the same construction rule every other adapter here follows.
|
|
142
|
+
*/
|
|
143
|
+
export function createRailsLineClassifier() {
|
|
144
|
+
let sawHeader = false;
|
|
145
|
+
return {
|
|
146
|
+
isHeader(line) {
|
|
147
|
+
if (isRailsHeaderLine(line)) {
|
|
148
|
+
sawHeader = true;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
},
|
|
153
|
+
isFrame(line) {
|
|
154
|
+
// Never from idle. A bare `x.rb:12:in 'foo'` is frame-shaped and is
|
|
155
|
+
// also an entirely ordinary thing for an application to log.
|
|
156
|
+
if (!sawHeader)
|
|
157
|
+
return false;
|
|
158
|
+
if (isRailsFrameLine(line))
|
|
159
|
+
return true;
|
|
160
|
+
// **A blank line must not clear the state**, and the first version of
|
|
161
|
+
// this classifier did clear it — which is why the whole grammar
|
|
162
|
+
// recognised every line correctly and still produced zero exceptions.
|
|
163
|
+
// `ExceptionBlockDetector` asks `isFrame` before `isBridge` in the
|
|
164
|
+
// `holding-header` state, so the blank line Rails prints between its
|
|
165
|
+
// header and its first frame arrived here first, reset `sawHeader`,
|
|
166
|
+
// and every real frame after it was refused. Recognising each line in
|
|
167
|
+
// isolation is not the same as recognising the sequence.
|
|
168
|
+
if (line.trim() === "")
|
|
169
|
+
return false;
|
|
170
|
+
sawHeader = false;
|
|
171
|
+
return false;
|
|
172
|
+
},
|
|
173
|
+
/**
|
|
174
|
+
* The whitespace-only line Rails prints between the header and the first
|
|
175
|
+
* frame. Consulted only while a header is held, so a blank line
|
|
176
|
+
* elsewhere in the log opens nothing.
|
|
177
|
+
*/
|
|
178
|
+
isBridge(line) {
|
|
179
|
+
return line.trim() === "";
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=rails-frame-parsing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rails-frame-parsing.js","sourceRoot":"","sources":["../src/rails-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AASH;;;;;;;;;;;;;;GAcG;AACH,MAAM,YAAY,GAAG,+EAA+E,CAAC;AAErG;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,GAAG,yDAAyD,CAAC;AAE9E,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,cAAc,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;AACpE,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB;IAKvC,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,OAAO;QACL,QAAQ,CAAC,IAAY;YACnB,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC5B,SAAS,GAAG,IAAI,CAAC;gBACjB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,CAAC,IAAY;YAClB,oEAAoE;YACpE,6DAA6D;YAC7D,IAAI,CAAC,SAAS;gBAAE,OAAO,KAAK,CAAC;YAC7B,IAAI,gBAAgB,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;YACxC,sEAAsE;YACtE,gEAAgE;YAChE,sEAAsE;YACtE,mEAAmE;YACnE,qEAAqE;YACrE,oEAAoE;YACpE,sEAAsE;YACtE,yDAAyD;YACzD,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,OAAO,KAAK,CAAC;YACrC,SAAS,GAAG,KAAK,CAAC;YAClB,OAAO,KAAK,CAAC;QACf,CAAC;QAED;;;;WAIG;QACH,QAAQ,CAAC,IAAY;YACnB,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes Rails' line-recognition functions with a `SourceLocationResolver`
|
|
3
|
+
* (RT-111).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a second parser and not a branch in the Ruby one
|
|
6
|
+
*
|
|
7
|
+
* The two grammars disagree about **where the raise site is**, which is the
|
|
8
|
+
* one thing `primaryFrameIndex` exists to state:
|
|
9
|
+
*
|
|
10
|
+
* - Ruby's header IS the innermost frame, so index 0 is the header line.
|
|
11
|
+
* - Rails' header carries **no location at all** — it is `Class (message):`
|
|
12
|
+
* — and the raise site is the *first backtrace line beneath it*.
|
|
13
|
+
*
|
|
14
|
+
* A single parser with a flag would have to carry both answers and pick one,
|
|
15
|
+
* which is the shape that produced RT-083 (`primaryFrameIndex` inherited
|
|
16
|
+
* rather than stated). Two parsers each state one answer.
|
|
17
|
+
*
|
|
18
|
+
* Both still report `primaryFrameIndex: 0`, and for Rails that is a
|
|
19
|
+
* different fact rather than the same one: **the header contributes no
|
|
20
|
+
* frame**, so frame 0 is the first line Rails printed, and Rails prints
|
|
21
|
+
* innermost-first. Measured against a live Rails 8.0.4 request pipeline:
|
|
22
|
+
* frame 0 is `ProbeController#read_customer_name`, the line that raised,
|
|
23
|
+
* with the controller action two frames below it and 40 framework frames
|
|
24
|
+
* under that.
|
|
25
|
+
*
|
|
26
|
+
* `fidelity` is `"synchronous"` for the same reason Ruby's is: every frame
|
|
27
|
+
* is a genuine link in the call chain that was executing.
|
|
28
|
+
*/
|
|
29
|
+
import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
|
|
30
|
+
export declare function createRailsStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
|
|
31
|
+
//# sourceMappingURL=rails-stack-trace-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rails-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/rails-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAIrG,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAkC9F"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes Rails' line-recognition functions with a `SourceLocationResolver`
|
|
3
|
+
* (RT-111).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a second parser and not a branch in the Ruby one
|
|
6
|
+
*
|
|
7
|
+
* The two grammars disagree about **where the raise site is**, which is the
|
|
8
|
+
* one thing `primaryFrameIndex` exists to state:
|
|
9
|
+
*
|
|
10
|
+
* - Ruby's header IS the innermost frame, so index 0 is the header line.
|
|
11
|
+
* - Rails' header carries **no location at all** — it is `Class (message):`
|
|
12
|
+
* — and the raise site is the *first backtrace line beneath it*.
|
|
13
|
+
*
|
|
14
|
+
* A single parser with a flag would have to carry both answers and pick one,
|
|
15
|
+
* which is the shape that produced RT-083 (`primaryFrameIndex` inherited
|
|
16
|
+
* rather than stated). Two parsers each state one answer.
|
|
17
|
+
*
|
|
18
|
+
* Both still report `primaryFrameIndex: 0`, and for Rails that is a
|
|
19
|
+
* different fact rather than the same one: **the header contributes no
|
|
20
|
+
* frame**, so frame 0 is the first line Rails printed, and Rails prints
|
|
21
|
+
* innermost-first. Measured against a live Rails 8.0.4 request pipeline:
|
|
22
|
+
* frame 0 is `ProbeController#read_customer_name`, the line that raised,
|
|
23
|
+
* with the controller action two frames below it and 40 framework frames
|
|
24
|
+
* under that.
|
|
25
|
+
*
|
|
26
|
+
* `fidelity` is `"synchronous"` for the same reason Ruby's is: every frame
|
|
27
|
+
* is a genuine link in the call chain that was executing.
|
|
28
|
+
*/
|
|
29
|
+
import { parseRailsFrame, parseRailsHeader } from "./rails-frame-parsing.js";
|
|
30
|
+
export function createRailsStackTraceParser(resolver) {
|
|
31
|
+
return {
|
|
32
|
+
async parse(rawText) {
|
|
33
|
+
const lines = rawText.split(/\r\n|\r|\n/);
|
|
34
|
+
const frames = [];
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
// The header is tried first and produces no frame — it has no
|
|
37
|
+
// location to produce one from. Skipping it explicitly rather than
|
|
38
|
+
// letting it fall through matters because `Class (message):` can
|
|
39
|
+
// contain a colon and a number in the message and would otherwise
|
|
40
|
+
// be worth checking against the frame pattern.
|
|
41
|
+
if (parseRailsHeader(line) !== null)
|
|
42
|
+
continue;
|
|
43
|
+
const raw = parseRailsFrame(line);
|
|
44
|
+
if (raw === null)
|
|
45
|
+
continue;
|
|
46
|
+
const location = await resolver.resolve({
|
|
47
|
+
file: raw.file,
|
|
48
|
+
line: raw.line,
|
|
49
|
+
column: null,
|
|
50
|
+
functionName: raw.functionName,
|
|
51
|
+
});
|
|
52
|
+
frames.push({ location, raw: line });
|
|
53
|
+
}
|
|
54
|
+
// Null, never an empty-but-structured StackTrace standing in for
|
|
55
|
+
// "couldn't parse" — this interface's contract, and the reason a
|
|
56
|
+
// header-only block does not become a zero-frame trace.
|
|
57
|
+
if (frames.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
return { fidelity: "synchronous", frames, primaryFrameIndex: 0 };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=rails-stack-trace-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rails-stack-trace-parser.js","sourceRoot":"","sources":["../src/rails-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE7E,MAAM,UAAU,2BAA2B,CAAC,QAAgC;IAC1E,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAe;YACzB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAE1C,MAAM,MAAM,GAAiB,EAAE,CAAC;YAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,8DAA8D;gBAC9D,mEAAmE;gBACnE,iEAAiE;gBACjE,kEAAkE;gBAClE,+CAA+C;gBAC/C,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,IAAI;oBAAE,SAAS;gBAE9C,MAAM,GAAG,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;gBAClC,IAAI,GAAG,KAAK,IAAI;oBAAE,SAAS;gBAE3B,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC;oBACtC,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,MAAM,EAAE,IAAI;oBACZ,YAAY,EAAE,GAAG,CAAC,YAAY;iBAC/B,CAAC,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,CAAC;YAED,iEAAiE;YACjE,iEAAiE;YACjE,wDAAwD;YACxD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YAErC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACnE,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby backtrace line shapes — the `LineClassifier` for
|
|
3
|
+
* `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
|
|
4
|
+
* feeds `SourceLocationResolver`.
|
|
5
|
+
*
|
|
6
|
+
* Ruby's format is architecturally its own shape, distinct from both V8 and
|
|
7
|
+
* CPython:
|
|
8
|
+
*
|
|
9
|
+
* 1. **The header line IS the innermost frame.** Where V8 prints a
|
|
10
|
+
* header with no location and CPython prints a banner with no location,
|
|
11
|
+
* Ruby's default uncaught-exception printer combines them:
|
|
12
|
+
* `file.rb:2:in 'delete_invoice': invoice 3 is corrupt (RuntimeError)` is
|
|
13
|
+
* simultaneously "here is the exception" and "here is where it happened."
|
|
14
|
+
* Confirmed verbatim against `Exception#full_message`'s own documented
|
|
15
|
+
* example (docs.ruby-lang.org/en/3.4/Exception.html): the array's first
|
|
16
|
+
* entry is `"t.rb:3:in 'Integer#/': divided by 0 (ZeroDivisionError)"`.
|
|
17
|
+
* 2. **Continuation frames are prefixed `from`, not `at`,** and carry no
|
|
18
|
+
* message: `\tfrom t.rb:10:in 'Object#bar'`. Confirmed against the same
|
|
19
|
+
* doc example and against ruby/spec's own
|
|
20
|
+
* `core/exception/top_level_spec.rb`, which asserts the custom-backtrace
|
|
21
|
+
* case renders as:
|
|
22
|
+
* ```
|
|
23
|
+
* /dir/foo.rb:10:in `raising': foo (RuntimeError)
|
|
24
|
+
* \tfrom /dir/bar.rb:20:in `caller'
|
|
25
|
+
* ```
|
|
26
|
+
* 3. **The closing quote is always a single quote; only the opening quote
|
|
27
|
+
* varies by Ruby version.** Ruby 3.4 (Feature #16495) replaced the
|
|
28
|
+
* opening backtick with a single quote and, per Feature #19117, prefixes
|
|
29
|
+
* the owning class where one has a permanent name
|
|
30
|
+
* (`'Object#division'` vs. the pre-3.4 `` `division' ``). Both are real,
|
|
31
|
+
* currently-deployed formats — Ruby 3.4 shipped 2024-12-25, and every
|
|
32
|
+
* LTS-adjacent 3.0–3.3 install in production still prints the backtick
|
|
33
|
+
* form — so both are accepted here via a `[` ']` opening-character class,
|
|
34
|
+
* never just the newer one.
|
|
35
|
+
* 4. **No `isTrailer` is needed.** Because the header already carries the
|
|
36
|
+
* exception's identity, Ruby has nothing analogous to CPython's
|
|
37
|
+
* identity-comes-last problem. Chained exceptions (`Exception#cause`)
|
|
38
|
+
* print as two complete header+frames blocks back to back with **no
|
|
39
|
+
* connective sentence between them** — confirmed against
|
|
40
|
+
* `top_level_spec.rb`'s `"the Exception#cause is printed to STDERR with
|
|
41
|
+
* backtraces"` example — so the second header simply starts a new block
|
|
42
|
+
* once the first one's last `from` line stops matching `isFrame`. No
|
|
43
|
+
* special-casing required, unlike CPython's connective-exclusion.
|
|
44
|
+
*
|
|
45
|
+
* **Known, disclosed gap:** a backtrace whose only frame is the header
|
|
46
|
+
* itself — a bare top-level `raise` with no caller beneath it, e.g.
|
|
47
|
+
* `ruby -e 'raise "boom"'` — has no `from` line to confirm it, and
|
|
48
|
+
* `ExceptionBlockDetector`'s `holding-header` state demotes an unconfirmed
|
|
49
|
+
* header to a plain log line at `flush()`. This is a structural
|
|
50
|
+
* consequence of a header-embeds-frame grammar meeting a classifier
|
|
51
|
+
* interface built around "header, then confirming frame line": there is no
|
|
52
|
+
* lookahead-free way to distinguish "a header about to be followed by
|
|
53
|
+
* frames" from "a header that IS the entire trace" from the header line
|
|
54
|
+
* alone. `parseRubyHeaderFrame` still extracts the correct single frame
|
|
55
|
+
* from such a line if handed it directly — the gap is confined to the
|
|
56
|
+
* streaming block detector's own decision to invoke the parser at all, not
|
|
57
|
+
* to the parser's capability. In practice this only affects genuinely
|
|
58
|
+
* flat top-level scripts; a backend process (the collector's real target)
|
|
59
|
+
* always has at least one caller frame between the raise site and the
|
|
60
|
+
* process entry point.
|
|
61
|
+
*/
|
|
62
|
+
export interface RawRubyFrame {
|
|
63
|
+
/** `<main>`, `block in foo`, `rescue in foo` and `Class#method`/`Class.method` are real Ruby frame names and are kept verbatim, never rewritten. */
|
|
64
|
+
readonly functionName: string | null;
|
|
65
|
+
readonly file: string;
|
|
66
|
+
readonly line: number;
|
|
67
|
+
}
|
|
68
|
+
export declare function isRubyHeaderLine(line: string): boolean;
|
|
69
|
+
export declare function isRubyFrameLine(line: string): boolean;
|
|
70
|
+
export declare function createRubyLineClassifier(): {
|
|
71
|
+
isHeader(line: string): boolean;
|
|
72
|
+
isFrame(line: string): boolean;
|
|
73
|
+
};
|
|
74
|
+
/** `null` when the header doesn't match Ruby's `file:line:in 'name': message` shape — never fabricated. */
|
|
75
|
+
export declare function parseRubyHeaderFrame(line: string): RawRubyFrame | null;
|
|
76
|
+
/** `null` for anything not shaped like a `from file:line:in 'name'` continuation — never fabricated. */
|
|
77
|
+
export declare function parseRubyFromFrame(line: string): RawRubyFrame | null;
|
|
78
|
+
//# sourceMappingURL=ruby-frame-parsing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-frame-parsing.d.ts","sourceRoot":"","sources":["../src/ruby-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAEH,MAAM,WAAW,YAAY;IAC3B,oJAAoJ;IACpJ,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAgBD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,wBAAwB,IAAI;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAChC,CAKA;AAED,2GAA2G;AAC3G,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAOtE;AAED,wGAAwG;AACxG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAOpE"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby backtrace line shapes — the `LineClassifier` for
|
|
3
|
+
* `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
|
|
4
|
+
* feeds `SourceLocationResolver`.
|
|
5
|
+
*
|
|
6
|
+
* Ruby's format is architecturally its own shape, distinct from both V8 and
|
|
7
|
+
* CPython:
|
|
8
|
+
*
|
|
9
|
+
* 1. **The header line IS the innermost frame.** Where V8 prints a
|
|
10
|
+
* header with no location and CPython prints a banner with no location,
|
|
11
|
+
* Ruby's default uncaught-exception printer combines them:
|
|
12
|
+
* `file.rb:2:in 'delete_invoice': invoice 3 is corrupt (RuntimeError)` is
|
|
13
|
+
* simultaneously "here is the exception" and "here is where it happened."
|
|
14
|
+
* Confirmed verbatim against `Exception#full_message`'s own documented
|
|
15
|
+
* example (docs.ruby-lang.org/en/3.4/Exception.html): the array's first
|
|
16
|
+
* entry is `"t.rb:3:in 'Integer#/': divided by 0 (ZeroDivisionError)"`.
|
|
17
|
+
* 2. **Continuation frames are prefixed `from`, not `at`,** and carry no
|
|
18
|
+
* message: `\tfrom t.rb:10:in 'Object#bar'`. Confirmed against the same
|
|
19
|
+
* doc example and against ruby/spec's own
|
|
20
|
+
* `core/exception/top_level_spec.rb`, which asserts the custom-backtrace
|
|
21
|
+
* case renders as:
|
|
22
|
+
* ```
|
|
23
|
+
* /dir/foo.rb:10:in `raising': foo (RuntimeError)
|
|
24
|
+
* \tfrom /dir/bar.rb:20:in `caller'
|
|
25
|
+
* ```
|
|
26
|
+
* 3. **The closing quote is always a single quote; only the opening quote
|
|
27
|
+
* varies by Ruby version.** Ruby 3.4 (Feature #16495) replaced the
|
|
28
|
+
* opening backtick with a single quote and, per Feature #19117, prefixes
|
|
29
|
+
* the owning class where one has a permanent name
|
|
30
|
+
* (`'Object#division'` vs. the pre-3.4 `` `division' ``). Both are real,
|
|
31
|
+
* currently-deployed formats — Ruby 3.4 shipped 2024-12-25, and every
|
|
32
|
+
* LTS-adjacent 3.0–3.3 install in production still prints the backtick
|
|
33
|
+
* form — so both are accepted here via a `[` ']` opening-character class,
|
|
34
|
+
* never just the newer one.
|
|
35
|
+
* 4. **No `isTrailer` is needed.** Because the header already carries the
|
|
36
|
+
* exception's identity, Ruby has nothing analogous to CPython's
|
|
37
|
+
* identity-comes-last problem. Chained exceptions (`Exception#cause`)
|
|
38
|
+
* print as two complete header+frames blocks back to back with **no
|
|
39
|
+
* connective sentence between them** — confirmed against
|
|
40
|
+
* `top_level_spec.rb`'s `"the Exception#cause is printed to STDERR with
|
|
41
|
+
* backtraces"` example — so the second header simply starts a new block
|
|
42
|
+
* once the first one's last `from` line stops matching `isFrame`. No
|
|
43
|
+
* special-casing required, unlike CPython's connective-exclusion.
|
|
44
|
+
*
|
|
45
|
+
* **Known, disclosed gap:** a backtrace whose only frame is the header
|
|
46
|
+
* itself — a bare top-level `raise` with no caller beneath it, e.g.
|
|
47
|
+
* `ruby -e 'raise "boom"'` — has no `from` line to confirm it, and
|
|
48
|
+
* `ExceptionBlockDetector`'s `holding-header` state demotes an unconfirmed
|
|
49
|
+
* header to a plain log line at `flush()`. This is a structural
|
|
50
|
+
* consequence of a header-embeds-frame grammar meeting a classifier
|
|
51
|
+
* interface built around "header, then confirming frame line": there is no
|
|
52
|
+
* lookahead-free way to distinguish "a header about to be followed by
|
|
53
|
+
* frames" from "a header that IS the entire trace" from the header line
|
|
54
|
+
* alone. `parseRubyHeaderFrame` still extracts the correct single frame
|
|
55
|
+
* from such a line if handed it directly — the gap is confined to the
|
|
56
|
+
* streaming block detector's own decision to invoke the parser at all, not
|
|
57
|
+
* to the parser's capability. In practice this only affects genuinely
|
|
58
|
+
* flat top-level scripts; a backend process (the collector's real target)
|
|
59
|
+
* always has at least one caller frame between the raise site and the
|
|
60
|
+
* process entry point.
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* The combined header+innermost-frame line. Distinguished from a bare
|
|
64
|
+
* `from` line by the trailing `: message` — real `from` lines never carry
|
|
65
|
+
* one (see `FROM_FRAME_PATTERN`). Not anchored to the start of the line,
|
|
66
|
+
* for the same reason V8's header pattern isn't: a backend that logs a
|
|
67
|
+
* caught exception's `full_message` through a leveled logger prefixes a
|
|
68
|
+
* timestamp, and Ruby's own top-level printer (which never adds a prefix)
|
|
69
|
+
* is only one of the two realistic capture paths.
|
|
70
|
+
*/
|
|
71
|
+
const HEADER_PATTERN = /(?<file>.+):(?<line>\d+):in\s+[`'](?<fn>[^']*)':\s/;
|
|
72
|
+
/** Same location+name grammar as the header, but `from`-prefixed and with no trailing message. */
|
|
73
|
+
const FROM_FRAME_PATTERN = /^\s*from\s+(?<file>.+):(?<line>\d+):in\s+[`'](?<fn>[^']*)'\s*$/;
|
|
74
|
+
export function isRubyHeaderLine(line) {
|
|
75
|
+
return HEADER_PATTERN.test(line);
|
|
76
|
+
}
|
|
77
|
+
export function isRubyFrameLine(line) {
|
|
78
|
+
return FROM_FRAME_PATTERN.test(line);
|
|
79
|
+
}
|
|
80
|
+
export function createRubyLineClassifier() {
|
|
81
|
+
return {
|
|
82
|
+
isHeader: isRubyHeaderLine,
|
|
83
|
+
isFrame: isRubyFrameLine,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** `null` when the header doesn't match Ruby's `file:line:in 'name': message` shape — never fabricated. */
|
|
87
|
+
export function parseRubyHeaderFrame(line) {
|
|
88
|
+
const match = HEADER_PATTERN.exec(line);
|
|
89
|
+
if (!match?.groups)
|
|
90
|
+
return null;
|
|
91
|
+
const file = match.groups["file"];
|
|
92
|
+
const lineNumber = Number(match.groups["line"]);
|
|
93
|
+
if (file === undefined || !Number.isInteger(lineNumber))
|
|
94
|
+
return null;
|
|
95
|
+
return { functionName: match.groups["fn"] ?? null, file, line: lineNumber };
|
|
96
|
+
}
|
|
97
|
+
/** `null` for anything not shaped like a `from file:line:in 'name'` continuation — never fabricated. */
|
|
98
|
+
export function parseRubyFromFrame(line) {
|
|
99
|
+
const match = FROM_FRAME_PATTERN.exec(line);
|
|
100
|
+
if (!match?.groups)
|
|
101
|
+
return null;
|
|
102
|
+
const file = match.groups["file"];
|
|
103
|
+
const lineNumber = Number(match.groups["line"]);
|
|
104
|
+
if (file === undefined || !Number.isInteger(lineNumber))
|
|
105
|
+
return null;
|
|
106
|
+
return { functionName: match.groups["fn"] ?? null, file, line: lineNumber };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=ruby-frame-parsing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-frame-parsing.js","sourceRoot":"","sources":["../src/ruby-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AASH;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,oDAAoD,CAAC;AAE5E,kGAAkG;AAClG,MAAM,kBAAkB,GAAG,gEAAgE,CAAC;AAE5F,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,wBAAwB;IAItC,OAAO;QACL,QAAQ,EAAE,gBAAgB;QAC1B,OAAO,EAAE,eAAe;KACzB,CAAC;AACJ,CAAC;AAED,2GAA2G;AAC3G,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9E,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9E,CAAC"}
|