scryer 1.2.1 → 1.3.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -10
  3. data/README.md +9 -4
  4. data/docs/architecture.md +45 -36
  5. data/docs/rails-integration.md +182 -1
  6. data/docs/rules.md +8 -1
  7. data/docs/usage.md +5 -2
  8. data/lib/generators/scryer/install_generator.rb +1 -0
  9. data/lib/generators/scryer/templates/scryer_initializer.rb +22 -0
  10. data/lib/scryer/ai_client.rb +1 -0
  11. data/lib/scryer/ai_fix_suggester.rb +1 -0
  12. data/lib/scryer/apm.rb +458 -0
  13. data/lib/scryer/ast.rb +53 -0
  14. data/lib/scryer/authorization_watcher.rb +1 -0
  15. data/lib/scryer/baseline.rb +1 -0
  16. data/lib/scryer/cache_extractor.rb +1 -0
  17. data/lib/scryer/cli.rb +19 -11
  18. data/lib/scryer/colorizer.rb +1 -0
  19. data/lib/scryer/dependency_audit.rb +1 -0
  20. data/lib/scryer/dependency_fixer.rb +1 -0
  21. data/lib/scryer/duplicate_detector.rb +1 -0
  22. data/lib/scryer/finding.rb +1 -0
  23. data/lib/scryer/fix_runner.rb +1 -0
  24. data/lib/scryer/fix_verifier.rb +1 -0
  25. data/lib/scryer/mechanical_fixer.rb +1 -0
  26. data/lib/scryer/method_extractor.rb +1 -0
  27. data/lib/scryer/minitest.rb +1 -0
  28. data/lib/scryer/performance_rules/inefficient_save_loop_rule.rb +1 -0
  29. data/lib/scryer/performance_rules/missing_pagination_rule.rb +1 -0
  30. data/lib/scryer/performance_rules/n_plus_one_query_rule.rb +2 -0
  31. data/lib/scryer/performance_rules/unbounded_table_scan_rule.rb +9 -1
  32. data/lib/scryer/query_extractor.rb +1 -0
  33. data/lib/scryer/query_watcher.rb +1 -0
  34. data/lib/scryer/railtie.rb +64 -4
  35. data/lib/scryer/report_renderer.rb +161 -99
  36. data/lib/scryer/rspec.rb +1 -0
  37. data/lib/scryer/rule.rb +15 -2
  38. data/lib/scryer/rule_set.rb +1 -0
  39. data/lib/scryer/rules/action_cable_forgery_protection_rule.rb +1 -0
  40. data/lib/scryer/rules/active_storage_inline_disposition_rule.rb +1 -0
  41. data/lib/scryer/rules/active_storage_missing_content_type_validation_rule.rb +1 -0
  42. data/lib/scryer/rules/authentication_bypass_rule.rb +1 -0
  43. data/lib/scryer/rules/command_injection_rule.rb +1 -0
  44. data/lib/scryer/rules/consider_all_requests_local_rule.rb +1 -0
  45. data/lib/scryer/rules/cors_misconfiguration_rule.rb +1 -0
  46. data/lib/scryer/rules/csrf_protection_rule.rb +1 -0
  47. data/lib/scryer/rules/dangerous_eval_rule.rb +117 -0
  48. data/lib/scryer/rules/force_ssl_rule.rb +1 -0
  49. data/lib/scryer/rules/graphql_missing_query_limits_rule.rb +1 -0
  50. data/lib/scryer/rules/hardcoded_basic_auth_rule.rb +1 -0
  51. data/lib/scryer/rules/hardcoded_secret_key_base_rule.rb +1 -0
  52. data/lib/scryer/rules/hardcoded_secret_rule.rb +1 -0
  53. data/lib/scryer/rules/host_authorization_disabled_rule.rb +1 -0
  54. data/lib/scryer/rules/idor_rule.rb +6 -14
  55. data/lib/scryer/rules/insecure_cookie_serializer_rule.rb +1 -0
  56. data/lib/scryer/rules/job_raw_params_rule.rb +1 -0
  57. data/lib/scryer/rules/jwt_insecure_rule.rb +1 -0
  58. data/lib/scryer/rules/mass_assignment_rule.rb +10 -16
  59. data/lib/scryer/rules/missing_authorization_rule.rb +1 -0
  60. data/lib/scryer/rules/missing_policy_scope_rule.rb +2 -9
  61. data/lib/scryer/rules/open_redirect_rule.rb +1 -0
  62. data/lib/scryer/rules/path_traversal_rule.rb +1 -0
  63. data/lib/scryer/rules/security_headers_rule.rb +1 -0
  64. data/lib/scryer/rules/sql_injection_rule.rb +1 -0
  65. data/lib/scryer/rules/ssrf_rule.rb +1 -0
  66. data/lib/scryer/rules/unsafe_deserialization_rule.rb +1 -0
  67. data/lib/scryer/rules/verbose_production_log_level_rule.rb +1 -0
  68. data/lib/scryer/rules/weak_crypto_rule.rb +1 -0
  69. data/lib/scryer/rules/weak_session_cookie_rule.rb +1 -0
  70. data/lib/scryer/rules/xss_unsafe_html_rule.rb +1 -0
  71. data/lib/scryer/scanner.rb +126 -7
  72. data/lib/scryer/style_rules/frozen_string_literal_rule.rb +1 -0
  73. data/lib/scryer/version.rb +2 -1
  74. data/lib/scryer.rb +49 -0
  75. data/lib/tasks/scryer.rake +8 -2
  76. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47762d5ce6faa0ec795a2f4966f61dee19625e4dd803e0219a0f47440c1d9892
4
- data.tar.gz: 9d892e071867b5ff6e2023bd0dfa0f1e98c4b7730b4652ad9d1a5bdaf204eb6b
3
+ metadata.gz: 3d7873062d73beb49e505138b100a9507d676b2d5a13838ba382fedb9ac31a2e
4
+ data.tar.gz: 6f9168a62835d3aba9a583720fcfea45c5271ddb24d16773023a298566522ad0
5
5
  SHA512:
6
- metadata.gz: 51b48a7aad2906b43d3d86a0a82a19275c7c6fb19860ec23da7671f765648d47ca29454e88d9ad959b4b8870ba91d1ed5b5590c0651306f8560889baa3acee0f
7
- data.tar.gz: a8d456858bca651e77ceb583304645f5f69ae827c6669c09efb9caf88eb707de72f66177faea7f935415095b72874c282c762b4a5db78252be10a90155708aa6
6
+ metadata.gz: 784555c1e3b96c43ec90f3151fc1beeb2abe90d1905dd897b4a40b33c44a63d00173a07705f44fbefbfb98c53342f328fffb57d1f2d5c196002ee30c3037b9c1
7
+ data.tar.gz: cf1e78fa3112a1ad8cf28a8686ec59a8ce5e221945ef0b0eb3d26a9ea5c8b943bc36e49547de6bf6e5770623c5e57f1377c603a8d50401585a88a00eedb0d3ad
data/CHANGELOG.md CHANGED
@@ -3,7 +3,126 @@
3
3
  All notable changes to this project are documented here. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
- ## [Unreleased]
6
+ ## [1.3.0] - 2026-09-21
7
+
8
+ - **Added: `Scryer::APM`, runtime method-level tracing with New Relic and OpenTelemetry
9
+ integration** (see README's "Runtime method tracing (APM)" section in
10
+ `docs/rails-integration.md`). Wraps configured classes' own methods (`Module#prepend`, the same
11
+ technique `QueryWatcher` already uses — no `TracePoint`) and, when a supported provider's SDK is
12
+ loaded, creates a real nested span per call via *that SDK's own* public API: New Relic via
13
+ `NewRelic::Agent::Tracer.start_segment`, OpenTelemetry via
14
+ `OpenTelemetry.tracer_provider.tracer(...).start_span` with `OpenTelemetry::Context.attach`/
15
+ `.detach` for correct parent/child nesting. Only `instrumentation: :selective` is implemented
16
+ (`:discovery`/`:deep_trace` raise a clear "not implemented yet" error rather than silently
17
+ no-opping); only `provider: :new_relic` and `provider: :opentelemetry` are implemented (no
18
+ Datadog, no other vendor SDK). Zero new gem dependencies — both providers are soft-detected
19
+ (`defined?(NewRelic::Agent::Tracer)` / `defined?(OpenTelemetry::Trace)`), never required; a host
20
+ app adds whichever SDK it wants to its own Gemfile. 29 tests in `test/apm_test.rb` (nesting/
21
+ parent-child spans for both providers, exception handling, kwargs/blocks/visibility
22
+ preservation, inherited-method exclusion, duplicate-instrumentation guard, sampling, redaction,
23
+ thread isolation, New Relic segment creation + exporter-failure safety, OpenTelemetry span
24
+ creation + start-failure safety, Rack middleware, config flow-through) plus a new
25
+ `benchmark/apm_overhead.rb` with measured (not estimated) per-call overhead numbers — see the
26
+ doc section for actual figures and what they do/don't cover. `Scryer::Railtie` now also
27
+ auto-wires APM (`require`/`Scryer::APM.enable!`/middleware installation) whenever
28
+ `c.apm.enabled = true`, so that flag plus `c.apm.include` is genuinely sufficient in an
29
+ initializer — no separate manual wiring needed. This boot-time path is held to a stricter safety
30
+ bar than the rest of the public API: any failure while auto-enabling (a bad `include` entry, an
31
+ unimplemented `instrumentation` mode, a `require` failure) is caught — `rescue Exception`, not
32
+ just `StandardError`, deliberately, since a packaging-related `LoadError` is a `ScriptError` —
33
+ logged, and leaves APM cleanly disabled rather than ever failing app boot. Likewise, a single
34
+ method that fails to instrument (an internal error, not a config typo) no longer aborts the rest
35
+ of that class or `include` list. `Scryer::APM.enable!` called directly (console, tests) is
36
+ unchanged and still raises loudly for an unimplemented mode — only the automatic Railtie path
37
+ degrades gracefully. 5 more tests in `test/railtie_apm_test.rb` cover that boot-time path
38
+ directly (34 new tests total).
39
+
40
+ ## [1.2.2] - 2026-09-12
41
+
42
+ - HTML report redesign: the top-of-report score panel is now a card (soft shadow, rounded
43
+ corners) instead of a flat box, the four grade badges are gradient-filled rounded squares
44
+ instead of plain circles, and the severity bars use a CSS grid for consistent label/track/count
45
+ alignment instead of flexbox eyeballing. Purely visual — no change to report content or data.
46
+ - Fixed: `Scryer::CLI#git` shelled out via backtick interpolation
47
+ (`` `git -C #{Shellwords.escape(root)} #{cmd}` ``) — flagged by Scryer's own `command_injection`
48
+ rule on a self-scan. Replaced with `Open3.capture3("git", "-C", root, *cmd)`, an argv array with
49
+ no shell involved at all, so there's nothing to escape or inject regardless of what `root`
50
+ contains. `cmd` callers now pass argv words instead of a pre-joined string.
51
+ - Added the `# frozen_string_literal: true` magic comment to every file under `lib/` that was
52
+ missing it (66 files) — this gem's own `frozen_string_literal` rule fired on itself.
53
+ - Fixed: `unbounded_table_scan` and `n_plus_one_query` false-positived on any bare
54
+ `Const.all`/`.where`/`.order`-shaped call, even when `Const` was a plain Ruby module/class with
55
+ no ActiveRecord ancestry (e.g. this gem's own `RuleSet.all.each`) — neither rule consulted
56
+ `known_models`/`known_non_models` at all. Both now gate their match through
57
+ `Ast.likely_model_name?`, same as `IdorRule` already did. `Scanner#collect_class_declarations`
58
+ also now records every `module X` declaration as a "definitely not a model" signal (a bare
59
+ module can never be `< ActiveRecord::Base`), alongside the existing no-superclass-class signal.
60
+ - **Breaking (report shape): one blended `Security Score` replaced with four independent
61
+ scores** — `security_score`, `performance_score`, `style_score`, and `dependency_score`
62
+ (`ReportRenderer#category_score` runs the same severity+confidence-weighted formula for each,
63
+ scoped to only that category's own findings; see `SCORE_SEVERITY_WEIGHT`/
64
+ `SCORE_CONFIDENCE_WEIGHT`). Previously `security_score` folded performance findings in at a
65
+ diluted weight and dropped style findings entirely, which meant a codebase could be
66
+ security-clean but drowning in N+1 queries and the console/HTML report would barely show it. Now
67
+ each category gets its own 0-100 score and letter grade, and none of them dilute or leak into
68
+ another: `scryer`'s console summary prints all four (`Security Score`/`Performance Score`/
69
+ `Style Score`/`Dependency Score`, each `N/100 (grade)`; `Dependency Score` shows
70
+ `skipped (--no-deps)`/`skipped (nodeps)` when the dependency audit didn't run), and the HTML
71
+ report's top panel shows four badges instead of one. `as_hash`/JSON output gains
72
+ `performance_score`/`style_score`/`dependency_score` keys alongside the existing
73
+ `security_score` (whose own meaning changed: it's now security findings only, no longer
74
+ security + dependency + diluted-performance combined). `rules_clean_rate` is unchanged (still one
75
+ aggregate rule-level pass rate across all three rule-backed categories together — a different,
76
+ complementary signal, not itself split per-category). Covered by a rewritten
77
+ `test/security_score_test.rb`, including a dedicated independence test proving a finding in one
78
+ category never affects another's score.
79
+ - Added `dangerous_eval` (new `Scryer::Rules::DangerousEvalRule`, CWE-95, critical severity): flags
80
+ `eval`/`instance_eval`/`class_eval`/`module_eval` called with anything other than a hardcoded
81
+ string literal with no interpolation — a real coverage gap (Scryer had no eval-injection check at
82
+ all), reported against a real `eval(singular_instance)` finding another scanner caught and Scryer
83
+ didn't. Deliberately broader than this gem's other injection rules (sql_injection/mass_assignment/
84
+ ssrf/path_traversal only fire when the argument traces back to `params` specifically): an
85
+ eval-family argument has essentially no legitimate reason to be anything but a literal the
86
+ developer wrote, so this fires on *any* non-literal argument — a local variable, an ivar, a method
87
+ call, an interpolated string — regardless of where the value actually came from. Confidence varies
88
+ the same way a taint-analysis scanner's own "Dangerous Eval" check varies confidence with its
89
+ taint engine (High vs Weak): `high` when `params` is directly visible in the argument's own expression (e.g.
90
+ `eval(params[:code])`), `medium` otherwise — the closest signal available without real data-flow
91
+ tracing. Severity stays `critical` either way regardless of confidence: the blast radius if the
92
+ argument is ever attacker-influenced doesn't depend on how sure Scryer is today, and `eval` is
93
+ never the *correct* way to do dynamic dispatch even when today's value happens to be safe
94
+ (`const_get`/`safe_constantize` do the same job with zero code-execution risk). A block-only call
95
+ (`obj.instance_eval { ... }`, no string argument) is never flagged. Covered by
96
+ `test/rule_fixtures_test.rb`, a new `test/dangerous_eval_rule_test.rb` (9 tests, including the
97
+ confidence split), and a new `benchmark/corpus.rb` entry (100%/100%/100% precision/recall/F1).
98
+ - Fixed: `mass_assignment`, `idor`, and `missing_policy_scope` false-positived on a plain Ruby
99
+ service/command object's constructor or class-level entry point (e.g. an internal
100
+ `Server.new(params).call` convention, or the equivalent `interactor`-gem-style
101
+ `SomeThingInteractor.call(params)`) — syntactically identical to `Order.new(params)` (a bare
102
+ constant, a `.new`/`.find`/`.where`-adjacent verb, a raw `params` argument), but none of these
103
+ calls write request data onto ActiveRecord attributes by themselves. Two fixes, in order of
104
+ precedence:
105
+ - **Project-wide model resolution** (`Scanner#call`, new): every scan now walks every
106
+ `class X < Y` declaration across every scanned file once, up front, and resolves which class
107
+ names actually descend from `ApplicationRecord`/`ActiveRecord::Base` (transitively — an
108
+ abstract per-shard base class or an STI subclass still resolves correctly), and which classes
109
+ are declared with *no* superclass at all or with a superclass matching `/\AApplication(?!Record\z)/`
110
+ (`ApplicationJob`, `ApplicationMailer`, a project's own `ApplicationService`, ...) — neither
111
+ shape an ActiveRecord model is ever declared with, so that's an unconditional "definitely not a
112
+ model" signal regardless of the class's name. This is the general fix: a service object with
113
+ *any* name is now excluded as long as it's declared somewhere in the scanned project, and a
114
+ real model is never wrongly excluded just because its name happens to look like a service
115
+ object (e.g. a model genuinely named `Command`). See `Ast.likely_model_name?` and
116
+ `Scanner#collect_class_declarations`/`#resolve_known_models`.
117
+ - **Naming-convention fallback** (unchanged from the original fix, only reached when a class
118
+ isn't declared anywhere Scryer scanned — a gem-provided constant, or outside `c.dirs`):
119
+ `Ast::NON_MODEL_RECEIVER_SUFFIXES` excludes constants ending in `Service`, `Server`,
120
+ `Interactor`, `Operation`, `Command`, or `UseCase`.
121
+ All three rules' previously-duplicated `NON_MODEL_RECEIVERS` exclusion lists are now one shared
122
+ `Ast::NON_MODEL_RECEIVER_NAMES`. Covered by new `test/mass_assignment_rule_test.rb` and
123
+ `test/scanner_known_models_test.rb` — no prior dedicated test file existed for `MassAssignmentRule`
124
+ at all, and no prior test exercised known-model resolution across files (inherently impossible to
125
+ test via a single-file `scan_with` call).
7
126
 
8
127
  ## [1.2.1] - 2026-09-04
9
128
 
@@ -357,10 +476,10 @@ All notable changes to this project are documented here. Format loosely follows
357
476
  `properties` and a combined severity+confidence `rank`). Performance/style rules get a
358
477
  `confidence` too (no CWE/OWASP — that taxonomy is security-specific). This is Scryer's own
359
478
  best-effort categorization for practitioner convenience, not an OWASP-endorsed or independently
360
- audited mapping — see the README's "What Scryer detects" section for the full caveat. Brakeman
361
- already tags CWE and reports a confidence level for its own warnings; this isn't a novel
362
- capability, just a fuller version of something that idea already existed elsewhere (see the
363
- comparison table's new footnote).
479
+ audited mapping — see the README's "What Scryer detects" section for the full caveat.
480
+ Taint-analysis scanners already tag CWE and report a confidence level for their own warnings;
481
+ this isn't a novel capability, just a fuller version of something that idea already existed
482
+ elsewhere (see the comparison table's new footnote).
364
483
  - New `ReportRenderer#owasp_coverage` — counts security findings per OWASP Top 10 category, shown
365
484
  as a console summary block ("OWASP Top 10 (2021) coverage:") and a new HTML report section, a
366
485
  direct byproduct of every security rule now carrying an `owasp_category`.
@@ -392,9 +511,9 @@ All notable changes to this project are documented here. Format loosely follows
392
511
  performed.
393
512
  - Repositioned the README, gemspec, docs site, and llms.txt around this: Scryer's differentiator
394
513
  isn't "one command instead of several tools," it's ranking risk *across* security, performance,
395
- dependencies, and code quality — something none of RuboCop/Brakeman/bundler-audit do even
396
- within their own domain, let alone across all four. The honest heuristic-vs-taint-analysis
397
- framing versus Brakeman (and the `idor` false-positive caveat) carries over unchanged from the
514
+ dependencies, and code quality — something none of RuboCop/a taint-analysis scanner/bundler-audit
515
+ do even within their own domain, let alone across all four. The honest heuristic-vs-taint-analysis
516
+ framing versus taint-analysis scanners (and the `idor` false-positive caveat) carries over unchanged from the
398
517
  1.0.0 comparison table — this is a reframing of what Scryer adds, not a new claim about
399
518
  detection accuracy.
400
519
 
@@ -451,8 +570,8 @@ All notable changes to this project are documented here. Format loosely follows
451
570
  ## [1.0.0] - 2026-08-13
452
571
 
453
572
  Seventeen new security rules, a SARIF report format, a Ruby end-of-life check, and a
454
- credentials-exposure check — see the README's "Scryer vs RuboCop vs Brakeman vs bundler-audit"
455
- section for how the new security coverage is positioned (heuristic pattern-matching, not taint
573
+ credentials-exposure check — see the README's "Scryer vs RuboCop vs a taint-analysis scanner vs
574
+ bundler-audit" section for how the new security coverage is positioned (heuristic pattern-matching, not taint
456
575
  analysis; `idor` in particular carries real false-positive risk by nature of the problem).
457
576
 
458
577
  - New security rules (all `category: "security"`, skippable individually via `--skip RULE_ID` /
data/README.md CHANGED
@@ -58,8 +58,11 @@ HTML report: tmp/scryer_report.html
58
58
  That's real output from a scan of a live 236-file Rails app (an internal production codebase we
59
59
  call "acme-app" here — file/controller names above are anonymized, since we don't publish that
60
60
  app's source; the finding counts, severities, rule IDs, and line numbers are exactly as scanned,
61
- unedited) — not a mockup, and the harsh grade is real too (see
62
- [Security score](docs/architecture.md#security-score) for what it does and doesn't mean). "Top priorities" is the same
61
+ unedited) — not a mockup, and the harsh grade is real too. This capture predates the current
62
+ version, which reports four independent scores (Security/Performance/Style/Dependency, each with
63
+ its own grade) instead of the single `Security Score` line shown above — see
64
+ [Scores](docs/architecture.md#scores) for the current format and what each one does and doesn't
65
+ mean. "Top priorities" is the same
63
66
  severity ranking [`ReportRenderer#top_risks`](lib/scryer/report_renderer.rb) applies across *all*
64
67
  categories — security, dependencies, performance, code quality — not just within each one; in the
65
68
  HTML report it's at the top of the Findings section, ahead of the 309 individual findings
@@ -73,7 +76,7 @@ below for exactly how this differs from what those tools do.
73
76
  security vulnerabilities, performance heuristics, dependency risk, and code-quality issues in one
74
77
  scan, ranked by severity across all four categories. See
75
78
  [What Scryer detects](docs/rules.md#what-scryer-detects).
76
- - **Is it Rails-specific?** Most of the 31 security rules target Rails conventions specifically
79
+ - **Is it Rails-specific?** Most of the 32 security rules target Rails conventions specifically
77
80
  (controllers, `config/environments/*`, Active Storage, Action Cable, `params`), but the scanning
78
81
  engine itself only needs Ruby source — no Rails app or database required to run it. See
79
82
  [Designed for Ruby](docs/rules.md#designed-for-ruby).
@@ -117,7 +120,9 @@ each one longer-form than a single section here, cross-linked from wherever it's
117
120
  performance numbers, the security model, false-positive handling, the accuracy benchmark, and
118
121
  the full Scryer vs RuboCop vs Brakeman vs bundler-audit comparison.
119
122
  - [**Rails Integration**](docs/rails-integration.md) — the runtime query/authorization watchers,
120
- the `scryer:install` generator, and testing Scryer's own results in your app's test suite.
123
+ runtime method-level tracing (New Relic or OpenTelemetry, for finding what an APM tool's own
124
+ "Other"/"Application Code" time actually is), the `scryer:install` generator, and testing
125
+ Scryer's own results in your app's test suite.
121
126
  - [**Contributing & Releasing**](docs/contributing.md) — how to add a new rule, and how this
122
127
  gem's own release process works.
123
128
 
data/docs/architecture.md CHANGED
@@ -24,7 +24,7 @@ no Rails/database boot.
24
24
 
25
25
  Parsing is the only real cost, and it's linear in file count/size — there's no Rails boot, no
26
26
  database connection, and (with `--no-deps`) no network call at all. A real, unscoped scan of a
27
- 236-file production Rails app (the same one behind the [Security score](#security-score) example
27
+ 236-file production Rails app (the same one behind the [Scores](#scores) example
28
28
  above) completes in about 2.5 seconds end to end on a laptop, `--no-deps` set. The one deliberately
29
29
  slower path is the [dependency audit](./rules.md#dependency-audit) (on by default): it queries
30
30
  [OSV.dev](https://osv.dev) once per unique gem in `Gemfile.lock`, so total time scales with distinct
@@ -91,55 +91,64 @@ normal for this class of tool, not a bug. An already-guarded call can still get
91
91
  rules don't trace surrounding conditionals — always review a finding in its surrounding context
92
92
  before acting on it.
93
93
 
94
- ## Security score
94
+ ## Scores
95
95
 
96
- Every scan produces a single 0-100 score plus a letter grade (A-F), shown in the console summary
97
- and as a badge at the very top of the HTML report:
96
+ Every scan produces **four independent 0-100 scores**, each with its own letter grade (A-F) one
97
+ per category — shown in the console summary and as four badges at the very top of the HTML report:
98
98
 
99
99
  ```
100
- Security Score: 11/100 (F)
100
+ Security Score: 11/100 (F)
101
+ Performance Score: 62/100 (D)
102
+ Style Score: 100/100 (A)
103
+ Dependency Score: 88/100 (B)
101
104
  ```
102
105
 
103
- The formula (`ReportRenderer#security_score` in `lib/scryer/report_renderer.rb`): every security
104
- and dependency finding costs points, weighted by both its severity *and* its confidence (a
105
- `low`-confidence `idor` finding costs less than a `high`-confidence `sql_injection` finding at the
106
- same severity); performance findings cost points too, but at a fifth of that weight (a slow app is
107
- a real cost, just not a *security* one, so it nudges the score rather than driving it). Combined
108
- via exponential decay from 100 rather than linear subtraction — one critical finding visibly moves
109
- the score (100 → ~86) without a handful of findings driving any real app straight to a hard-clamped
110
- 0.
111
-
112
- Two things worth being precise about before you treat this number as meaningful:
113
-
114
- - **It's not normalized by app size.** A 10-file app and a 1,000-file app with the same finding
115
- *density* will score very differently here — this score reflects a scan's absolute finding
116
- exposure, not a rate. That makes it useful for tracking *one project's own trend* over time (did
117
- the next scan score higher or lower), not for comparing two differently-sized codebases against
118
- each other.
119
- - **Code-quality findings are never part of it.** Duplicate-code groups and the one style check
120
- (`frozen_string_literal`) are cosmetic, not risk, and stay out entirely a duplicate-code group
121
- isn't even backed by a `Finding` with a severity/confidence to weigh in the first place (see
122
- `Scryer::DuplicateDetector`). Check the "Code Quality" row in the summary box separately for
123
- that; performance findings, unlike code-quality ones, do count toward the score (lightly — see
124
- above).
125
-
126
- Alongside the score, every scan also reports a **rule-level pass rate** "how many of Scryer's
127
- registered checks fired zero findings":
106
+ The formula is the same for all four (`ReportRenderer#category_score` in
107
+ `lib/scryer/report_renderer.rb`, called once per category by `#security_score`/
108
+ `#performance_score`/`#style_score`/`#dependency_score`): every finding *in that category* costs
109
+ points, weighted by both its severity *and* its confidence (a `low`-confidence `idor` finding costs
110
+ less than a `high`-confidence `sql_injection` finding at the same severity), combined via
111
+ exponential decay from 100 rather than linear subtraction — one critical finding visibly moves that
112
+ category's score (100 → ~86) without a handful of findings driving it straight to a hard-clamped 0.
113
+ Crucially, the four scores never mix: a finding only ever affects the score for its own category, so
114
+ a codebase that's security-clean but drowning in N+1 queries gets an honest `Performance Score: F`
115
+ instead of that risk being invisible or diluted inside a single blended number. (Earlier versions of
116
+ this gem had exactly one combined score that folded performance findings in at a diluted weight and
117
+ dropped style findings entirely that's what these four replaced.)
118
+
119
+ Two things worth being precise about before you treat these numbers as meaningful:
120
+
121
+ - **None of them are normalized by app size.** A 10-file app and a 1,000-file app with the same
122
+ finding *density* will score very differently here each score reflects that category's absolute
123
+ finding exposure in this scan, not a rate. That makes them useful for tracking *one project's own
124
+ trend* over time (did the next scan score higher or lower, in this category), not for comparing
125
+ two differently-sized codebases against each other.
126
+ - **Duplicate-code groups have no score of their own.** A `DuplicateDetector::DuplicateGroup` isn't
127
+ even a `Finding` — no severity/confidence to weigh in the first place (see
128
+ `Scryer::DuplicateDetector`) — so it's excluded from all four; check the "Duplicate code" row in
129
+ the summary box directly for that. `Style Score` today is really just `frozen_string_literal`, the
130
+ one rule-based style check this gem has.
131
+
132
+ Alongside the four scores, every scan also reports one overall **rule-level pass rate** — "how many
133
+ of Scryer's registered checks fired zero findings":
128
134
 
129
135
  ```
130
136
  Checks: 23/36 rules clean (63.9%)
131
137
  ```
132
138
 
133
139
  This is the closest thing Scryer has to Brakeman's "X checks, Y warnings" framing — a coverage
134
- signal, not a risk signal. It's deliberately a *different* number from the security score, and the
135
- two won't always agree: a codebase can have a high clean rate (few distinct rules ever fire) and
136
- still a low score (the few that did fire were severe and high-confidence), or the reverse (many
137
- different rules each firing once, none of them serious). `ReportRenderer#rules_clean_rate` computes
140
+ signal, not a risk signal. It's deliberately a *different* number from the four scores above (and
141
+ spans all three rule-backed categories together, rather than being split per-category itself), and
142
+ they won't always agree: a codebase can have a high clean rate (few distinct rules ever fire) and
143
+ still a low score in one category (the few that did fire there were severe and high-confidence), or
144
+ the reverse (many different rules each firing once, none of them serious). `ReportRenderer#rules_clean_rate` computes
138
145
  it from `Scryer::RuleSet.all` (every registered security/performance/style rule) against which
139
146
  rule_ids actually appeared in this scan's findings — dependency checks aren't counted here since
140
147
  they're not backed by a `Scryer::Rule` subclass. Report both; neither alone tells the whole story.
141
148
 
142
- The 10/F above is a real score from the acme-app example, not a cherry-picked good result — see
149
+ The Security Score above is illustrative of the format (four independent scores, one letter grade
150
+ each) rather than one specific captured scan — see the README's own top-of-page example for a real,
151
+ unmodified `Security Score: 10/100 (F)` from an actual live app, and
143
152
  [A note on how this gem was actually verified](#a-note-on-how-this-gem-was-actually-verified) for
144
153
  why every example in this README is real output.
145
154
 
@@ -245,7 +254,7 @@ sits alongside them and adds the cross-category picture neither one (nor bundler
245
254
  four categories) by severity, shown as "Top priorities" in the console summary and at the top of
246
255
  the HTML report — see the example near the top of this README.
247
256
  - **[k]** Not a Scryer-only capability — Brakeman's own warnings already include a CWE reference.
248
- Scryer's version is a fuller mapping (every one of its 31 security rules carries both a CWE ID
257
+ Scryer's version is a fuller mapping (every one of its 32 security rules carries both a CWE ID
249
258
  and an OWASP Top 10 (2021) category, aggregated into an OWASP coverage scorecard in every
250
259
  report), but the underlying idea isn't new; see
251
260
  [What Scryer detects](./rules.md#what-scryer-detects) for the honesty caveat on how this mapping was built
@@ -91,6 +91,187 @@ Two honesty points worth being precise about:
91
91
  this class doesn't attempt.
92
92
 
93
93
 
94
+ ## Runtime method tracing (APM)
95
+
96
+ The problem `QueryWatcher` and `AuthorizationWatcher` don't touch: once a request lands inside
97
+ application code — a service object, a repository, a background-job method — most APM tools
98
+ (New Relic included) can only see it as one undifferentiated block of "Application Code" or
99
+ "Other" time. `Scryer::APM` answers "which method inside that block actually took the time" by
100
+ wrapping configured classes' own methods (the same `Module#prepend` technique `QueryWatcher` uses
101
+ on `ActiveRecord::QueryMethods`, not `TracePoint`) and, when a supported APM agent/SDK is loaded,
102
+ creating a real nested span per call via *that agent's own* public API — never an undocumented
103
+ endpoint or a separate exporter of Scryer's own. **Two providers are implemented:**
104
+
105
+ - **`:new_relic`** — a real nested segment per call via
106
+ `NewRelic::Agent::Tracer.start_segment`/`Segment#finish`, the same mechanism `add_method_tracer`
107
+ itself uses internally.
108
+ - **`:opentelemetry`** — a real nested span per call via
109
+ `OpenTelemetry.tracer_provider.tracer(...).start_span`, with
110
+ `OpenTelemetry::Context.attach`/`.detach` used to make each span the "current" one so the next
111
+ nested instrumented call picks it up as its parent automatically — the SDK's own documented
112
+ manual-span API (the one used whenever the block form, `Tracer#in_span`, doesn't fit).
113
+
114
+ Neither SDK is a hard dependency of this gem (see the gemspec: zero runtime deps beyond stdlib) —
115
+ both are soft-detected via `defined?(...)`. If you set `c.apm.provider = :opentelemetry`, add
116
+ `opentelemetry-sdk` (plus whatever exporter you want, e.g. `opentelemetry-exporter-otlp`) to your
117
+ own app's `Gemfile`, exactly the way you already add `newrelic_rpm` yourself to use `:new_relic`.
118
+ If the configured provider's SDK isn't loaded, spans are still traced (available via
119
+ `Scryer::APM.on_span`) but nothing is exported anywhere — a warning is logged once, the
120
+ instrumented methods keep working normally either way.
121
+
122
+ ```ruby
123
+ # config/initializers/scryer.rb
124
+ Scryer.configure do |c|
125
+ c.apm.enabled = true
126
+ c.apm.provider = :opentelemetry # or :new_relic
127
+ c.apm.instrumentation = :selective # only mode implemented so far — see Limitations below
128
+ c.apm.include = %w[BookingService PaymentService InventoryService]
129
+ c.apm.exclude = %w[] # Scryer:: / ActiveRecord:: / ActionController:: / ActionDispatch:: / Rails::
130
+ # are always excluded regardless of this list
131
+ c.apm.sampling_rate = 0.10 # 0.0–1.0, decided once per request, not per span
132
+ c.apm.capture_exception_messages = false # class name only by default; see Redaction below
133
+ end
134
+ ```
135
+
136
+ That's genuinely the whole thing — `c.apm.enabled = true` is sufficient by itself, no separate
137
+ `require`/`.enable!`/`middleware.use` calls needed. `Scryer::Railtie` (loaded automatically as
138
+ soon as the gem is in your `Gemfile`) checks `c.apm.enabled` in a Rails initializer that runs
139
+ *after* every file in `config/initializers/` — including this one — has already run, and does the
140
+ `require "scryer/apm"` / `Scryer::APM.enable!` / `Rails.application.config.middleware.use
141
+ Scryer::APM::Middleware` sequence for you (`Scryer::Railtie.maybe_enable_apm`, tested directly in
142
+ `test/railtie_apm_test.rb`) if and only if that flag is true. Leaving `c.apm.enabled` at its
143
+ default (`false`) — or omitting the `c.apm` block entirely — means none of this runs and there is
144
+ no measurable overhead, same as never having required this file at all.
145
+
146
+ **The one thing that genuinely can't be defaulted: `c.apm.include` must still be populated.**
147
+ There is no "trace every method automatically" mode — an empty `include` (the default) means
148
+ `enabled = true` turns the machinery on but instruments nothing, by design (see Production safety
149
+ below for why "instrument every method in the app" is deliberately not offered).
150
+
151
+ Outside a request (a Sidekiq job, a rake task), open a trace scope yourself the same way
152
+ `QueryWatcher.watch` works for queries:
153
+
154
+ ```ruby
155
+ Scryer::APM.trace { SomeJob.new.perform }
156
+ ```
157
+
158
+ ### What gets instrumented, precisely
159
+
160
+ `c.apm.include` takes class/module name strings. For each one, **only methods defined directly on
161
+ that class** (`instance_methods(false)` + private/protected equivalents) are wrapped — methods
162
+ *inherited* from a superclass or module are never touched, even if the named class itself has none
163
+ of its own. This is deliberate, not a current limitation: instrumenting inherited methods via a
164
+ bare class-name entry is exactly how `include: ["SomeModel"]` would end up silently wrapping
165
+ `ActiveRecord::Base` internals, which is the "sensitive framework internals" risk a production
166
+ instrumentation tool has to avoid by construction, not by asking users to get their `exclude` list
167
+ right. Method visibility (public/private/protected) is preserved exactly. Positional args, keyword
168
+ args, blocks, and return values all pass through `super` unchanged; exceptions of every class
169
+ (not just `StandardError`) are recorded on the span and always re-raised, never swallowed.
170
+
171
+ ### What each span records
172
+
173
+ `class_name`, `method_name`, `file`/`line` (captured once when the method is wrapped, not per
174
+ call), `trace_id`/`span_id`/`parent_span_id` (nested calls get correct parent/child relationships
175
+ via a thread-local span stack — see the "nested calls" test in `test/apm_test.rb`), `duration_ms`,
176
+ `status` (`:ok`/`:error`), `exception_class`, `thread_id`. **Method argument values, local
177
+ variables, and return values are never recorded, under any configuration** — there is no opt-in
178
+ that changes this. `exception_message` is `nil` unless `capture_exception_messages: true`, and even
179
+ then passes through a best-effort (not exhaustive — see Limitations) redaction pass that masks
180
+ long token/hash-shaped substrings and email addresses before being kept.
181
+
182
+ ### Production safety
183
+
184
+ - **`instrumentation: :off`** installs no hooks at all — measured overhead is within noise of not
185
+ requiring the file (see `benchmark/apm_overhead.rb`).
186
+ - **Sampling is per-trace, not per-span**: the decision is made once when `Scryer::APM.trace` opens
187
+ (or on the first instrumented call if a request somehow reaches one without a scope), so a
188
+ sampled request gets a complete, connected trace rather than a random scatter of orphaned spans.
189
+ - **Provider failures never break the traced method.** New Relic's `start_segment`/`finish`/
190
+ `notice_error` and OpenTelemetry's `start_span`/`finish`/`record_exception` are each wrapped in
191
+ their own `rescue StandardError` — see `test_new_relic_segment_start_failure_does_not_break_the_traced_method`
192
+ and `test_open_telemetry_span_start_failure_does_not_break_the_traced_method` in
193
+ `test/apm_test.rb`, which stub a failing `Tracer.start_segment`/`Tracer#start_span` and confirm
194
+ the real method's return value still comes through unaffected.
195
+ - **No custom export queue, batching, or retry/backoff exists in this module, and none is needed**:
196
+ spans are handed directly to the already-async, already-batched, already-retried agent/SDK
197
+ transport via its own span API — Scryer::APM never talks to a network endpoint itself. The one
198
+ thing this means: if your configured provider isn't loaded but you keep `Scryer::APM.on_span`
199
+ wired to your own sink, *that* sink is your responsibility to make non-blocking/bounded — nothing
200
+ here queues or batches on your behalf.
201
+ - **Calling `.instrument_by_name`/`.instrument_method` more than once for the same class/method is a
202
+ no-op** (a process-wide dedupe guard), so re-running an initializer (e.g., Rails' `to_prepare` in
203
+ development) can't double-wrap a method into two nested spans for one call.
204
+ - **A misconfiguration can never fail app boot, even in production.** This is the specific
205
+ guarantee `test/railtie_apm_test.rb` exists to prove, not just assert: a bad class name in
206
+ `include`, `instrumentation` set to an unimplemented mode, or any other failure while
207
+ `Scryer::Railtie` auto-enables APM (see "That's genuinely the whole thing" above) is caught —
208
+ broadly, `rescue Exception` rather than just `StandardError`, since a `require` failure from a
209
+ packaging issue would raise `LoadError`, a `ScriptError` — logged once, and leaves APM cleanly
210
+ disabled for that process; the rest of the app boots and runs normally. One method failing to
211
+ instrument (an internal error, not a bad config value) is scoped even tighter: only that method
212
+ is skipped, not the rest of its class or the rest of `include`. **This graceful-degradation bar
213
+ is specific to the automatic Railtie path** — `Scryer::APM.enable!` called directly (a console, a
214
+ test, a non-Rails script) still raises immediately for an unimplemented mode, which is the
215
+ correct behavior when a developer is watching and wants fast feedback rather than a silently
216
+ disabled feature.
217
+
218
+ ### Measured overhead
219
+
220
+ From `benchmark/apm_overhead.rb` (Apple silicon dev machine, Ruby 3.4.8, 200,000 calls per
221
+ scenario — re-run this yourself before trusting these numbers for capacity planning on different
222
+ hardware):
223
+
224
+ | scenario | ns/call | vs. uninstrumented |
225
+ |---|---|---|
226
+ | baseline (no Scryer::APM at all) | ~28 ns | 1.0x |
227
+ | `instrumentation: :off` | ~28 ns | ~1.0x (no measurable difference) |
228
+ | `:selective`, inside a trace, `sampling_rate: 0.0` | ~172 ns | ~6.3x |
229
+ | `:selective`, inside a trace, `sampling_rate: 1.0` (full span) | ~1771 ns | ~64x |
230
+
231
+ The `:off` vs. unsampled-but-wrapped gap (~6x) is the fixed cost of the `prepend`/`define_method`
232
+ dispatch layer itself, before any span is even considered — this is the floor cost paid by every
233
+ call to a wrapped method regardless of sampling, which is why `include` should name specific
234
+ service/business-logic classes, not broad namespaces. Full span creation (~1771 ns, ~1.8 µs) is
235
+ still small in absolute terms next to typical service-method bodies (a DB query alone is usually
236
+ 1–20+ ms), but at high call volume with `sampling_rate: 1.0` it is not free — this is exactly what
237
+ `sampling_rate` exists to bound. Retained-memory cost: 50,000 held `Span` objects (worst case — a
238
+ consumer that doesn't drain them) measured ~53 MB RSS growth, roughly ~1 KB/span.
239
+
240
+ ### Limitations — implemented vs. not yet
241
+
242
+ **Implemented and tested** (see `test/apm_test.rb`, 29 examples, and `test/railtie_apm_test.rb`, 5
243
+ more): `:off` and `:selective` modes; nested parent/child spans; exception recording + re-raise;
244
+ keyword args/blocks/return values; method visibility preservation; inherited-method exclusion;
245
+ duplicate-instrumentation guard; per-class `exclude`; trace-scoped sampling; thread isolation
246
+ (concurrent requests on different threads never share trace/span state); New Relic segment
247
+ creation, error notification, and exporter-failure safety; OpenTelemetry span creation
248
+ (with correct parent/child nesting via `Context.attach`/`.detach`), exception recording, and
249
+ start-failure safety; the Rack middleware; reading config through `Scryer.configure`;
250
+ `c.apm.enabled = true` alone (no manual `require`/`.enable!`/`middleware.use`) genuinely
251
+ triggering tracing via the Railtie; a bad `include` entry or an unimplemented mode failing to
252
+ crash boot (see "Production safety" above); a single method's instrumentation failure not taking
253
+ out the rest of its class.
254
+
255
+ **Not implemented** — raises a clear `ArgumentError` naming this doc rather than silently
256
+ no-opping if configured:
257
+ - `instrumentation: :discovery` and `:deep_trace` modes (low-overhead profiling to *find* expensive
258
+ methods automatically, and extra detail captured only for slow/failed requests, respectively).
259
+ - Any provider other than `:new_relic` and `:opentelemetry` — no Datadog, no vendor-specific SDK
260
+ beyond those two.
261
+ - Singleton (class) method instrumentation — only instance methods.
262
+ - Distributed trace-context propagation across a process boundary (an outgoing HTTP call to
263
+ another service, or an inbound request that already carries a W3C `traceparent` header). This
264
+ module's own `trace_id`/`span_id` fields are local-process only. The `:opentelemetry` provider's
265
+ *spans* do still nest correctly with each other within one process via `Context.attach`/`.detach`
266
+ — what's missing is reading/writing the `traceparent` header itself to connect that local trace
267
+ to one already in progress elsewhere.
268
+
269
+ **Known caveat, not a bug**: `Scryer::APM.disable!` (test-only) clears configuration/dedupe state
270
+ but cannot un-`prepend` an already-instrumented class — Ruby has no supported way to do that. Tests
271
+ that need a clean slate instrument a freshly-defined, uniquely-named class per example rather than
272
+ reusing one across examples; a host app calling `.enable!` more than once in a running process
273
+ should expect the same constraint.
274
+
94
275
  ## Generators
95
276
 
96
277
  Scryer ships one generator: `scryer:install`. Run `bin/rails generate scryer:install --help`
@@ -152,7 +333,7 @@ end
152
333
  `Scryer.scan(root:)` runs the same static scan `scryer:report`/the `scryer` executable do (using
153
334
  `c.dirs`/`c.skip_rules` from your initializer), without needing a report written to disk —
154
335
  `have_no_critical_findings`/`assert_no_critical_scryer_findings` only look at *security* findings
155
- (same scoping as the [security score](./architecture.md#security-score)); `have_no_findings_for`/
336
+ (same scoping as the [Security Score](./architecture.md#scores)); `have_no_findings_for`/
156
337
  `assert_no_scryer_findings_for` check a specific `rule_id` across all three static categories,
157
338
  for pinning a specific bug so it can't come back unnoticed. This talks to the live filesystem on
158
339
  every test run (a real `Ripper`-based scan, same cost as running `scryer` itself) — for a large app
data/docs/rules.md CHANGED
@@ -9,6 +9,13 @@
9
9
  * SQL injection
10
10
  * Mass assignment
11
11
  * Command injection
12
+ * Dangerous `eval` (`eval`/`instance_eval`/`class_eval`/`module_eval` called with anything other
13
+ than a hardcoded string literal) — broader than most other checks here: it fires on any
14
+ non-literal argument, not just one tracing back to `params`, since there's essentially no
15
+ legitimate reason for an eval-family argument to be anything else. Always `critical` severity;
16
+ confidence is `high` when `params` is directly visible in the argument, `medium` otherwise (the
17
+ closest signal available without real data-flow tracing — same axis Brakeman's own "Dangerous
18
+ Eval" check varies High/Weak confidence on)
12
19
  * Hardcoded secrets (and hardcoded HTTP Basic Auth credentials)
13
20
  * Unsafe deserialization
14
21
  * XSS-prone HTML
@@ -157,7 +164,7 @@ Generate detailed JSON or self-contained HTML reports:
157
164
  scryer -o report.json -o report.html
158
165
  ```
159
166
 
160
- The HTML report leads with a [security score](./architecture.md#security-score) badge and a severity distribution
167
+ The HTML report leads with four [score](./architecture.md#scores) badges and a severity distribution
161
168
  chart, then Overview/Summary/OWASP coverage, collapsed-by-default reference tables (every rule
162
169
  that *can* fire, not a wall of always-expanded detail), and the Findings section itself — Top
163
170
  priorities first, then a text filter box (rule/file/message, no page reload) above the full
data/docs/usage.md CHANGED
@@ -61,7 +61,7 @@ example — followed by where each report was written.
61
61
 
62
62
  **Color.** The summary box, top priorities, and `scryer fix`/`scryer verify` output are colored
63
63
  automatically at a real terminal — severity labels (`critical`/`warning`/`info` → red/yellow/cyan),
64
- the security score's letter grade (A/B green, C yellow, D/F red), fixed/skipped status lines, and
64
+ each score's letter grade (A/B green, C yellow, D/F red), fixed/skipped status lines, and
65
65
  so on. Off automatically whenever it would be wrong to color: piped/redirected output (e.g. `-o -`
66
66
  to a file, or piped to `less` without `-R`), [`NO_COLOR`](https://no-color.org) set, or
67
67
  `TERM=dumb`. `--color`/`--no-color` force it either way regardless of any of that — e.g. `scryer
@@ -282,7 +282,10 @@ scryer --baseline tmp/scryer_baseline.json -o scryer.sarif
282
282
  ```
283
283
  Baseline: tmp/scryer_baseline.json — showing new findings only (2 fixed since baseline).
284
284
 
285
- Security Score: 96/100 (A)
285
+ Security Score: 96/100 (A)
286
+ Performance Score: 90/100 (A)
287
+ Style Score: 100/100 (A)
288
+ Dependency Score: 88/100 (B)
286
289
 
287
290
  Security 1 finding
288
291
  ...
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "rails/generators"
2
3
 
3
4
  module Scryer
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Configuration for the scryer gem (static code analysis: security,
2
3
  # duplicate code, performance heuristics). Run `bin/rails scryer:report`
3
4
  # to scan this app and generate tmp/scryer_report.{json,html} locally.
@@ -41,3 +42,24 @@ end
41
42
  # Scryer::QueryWatcher.enable!
42
43
  # Rails.application.config.middleware.use Scryer::QueryWatcher::Middleware
43
44
  # end
45
+
46
+ # Runtime method-level tracing (see README's "Runtime method tracing (APM)")
47
+ # — finds which method inside a request is actually responsible for time
48
+ # an APM tool would otherwise lump into "Other"/"Application Code". Providers
49
+ # :new_relic and :opentelemetry (each soft-detected; no hard dependency added
50
+ # by enabling this — add whichever SDK you pick to your own Gemfile) and
51
+ # instrumentation :selective (named classes only) are implemented so far.
52
+ # `c.apm.enabled = true` below is genuinely sufficient on its own —
53
+ # Scryer::Railtie wires up the require/.enable!/middleware for you once this
54
+ # flag is true, after this file has already run. Safe to leave on in
55
+ # production — leaving `include` empty (or `enabled` false) has no
56
+ # measurable overhead, and sampling_rate bounds the cost of the classes you
57
+ # do name:
58
+ #
59
+ # Scryer.configure do |c|
60
+ # c.apm.enabled = true
61
+ # c.apm.provider = :opentelemetry # or :new_relic
62
+ # c.apm.instrumentation = :selective
63
+ # c.apm.include = %w[] # e.g. %w[BookingService PaymentService] — required, see README
64
+ # c.apm.sampling_rate = 0.10
65
+ # end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "json"
2
3
  require "net/http"
3
4
  require "uri"