rails_error_dashboard 0.8.0 → 0.8.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 002c2e5d338c585425599802271a90c5bac425dc86867da1aa10a0c7a478d1e8
4
- data.tar.gz: 077d7d7a7f957bee799cf39c96632532e3c74259e83310605c6068d57436aec3
3
+ metadata.gz: 532921b0ba2ccb40a532a66e5e3c7b1fcf17fc56e53cb5b0ed3772648644f0c0
4
+ data.tar.gz: 282169f161d90326aaebce5933521cd54c8bb373611e9002b447f114b6654204
5
5
  SHA512:
6
- metadata.gz: 8e6ef9f87aae8c200e997bf761184eb37c86d0637dfdd383fd6cc003f3c3b9fa30b7906fbaf6377670d932a60da5a9046202d4a48ab63264ce916c861ac20b91
7
- data.tar.gz: 67858acc12ad29cb412b0c049b776b7193c9ce8d45febfa4bc23e079a1afd7ff64a2a85bd81516c999cc57861631ed3f89ea62bc36544370811e8cff858c8678
6
+ metadata.gz: 63c2ba5ef4319eaff8450837fec526135e37865ea0d2c72c30b7010ff6e86c03e49f728172d032bec6ad626f8dba39d00841300839fee9b8a493caa912669559
7
+ data.tar.gz: c84238b00e2f6959fd1e4ac5a1c9ff2d696a0d28379a102e99d761585caf4c24a4f6446f4d303d69c933e5135b31e7c3ef1d0a6b7464376e88ad25b14068c75a
data/README.md CHANGED
@@ -233,7 +233,7 @@ Payload contract matches the `LlmCallEvent` value object — see [`docs/LLM_OBSE
233
233
  </details>
234
234
 
235
235
  <details>
236
- <summary><strong>Issue Tracking — GitHub, GitLab, Codeberg</strong></summary>
236
+ <summary><strong>Issue Tracking — GitHub, GitLab, Codeberg, Linear</strong></summary>
237
237
 
238
238
  One switch connects errors to your issue tracker. Platform becomes the source of truth — status, assignees, labels, and comments are mirrored live in the dashboard.
239
239
 
@@ -250,6 +250,17 @@ config.issue_tracker_token = ENV["RED_BOT_TOKEN"]
250
250
  # That's it — provider and repo auto-detected from git_repository_url
251
251
  ```
252
252
 
253
+ Linear works too — it's not a git forge, so set the provider and team key explicitly:
254
+
255
+ ```ruby
256
+ config.enable_issue_tracking = true
257
+ config.issue_tracker_provider = :linear
258
+ config.issue_tracker_repo = "ENG" # Linear team key (issues land as ENG-123)
259
+ config.issue_tracker_token = ENV["RED_BOT_TOKEN"] # lin_api_... personal API key
260
+ ```
261
+
262
+ Closing maps to the team's first `completed` workflow state, reopening to `unstarted`/`backlog`. Two-way sync uses Linear webhooks (`Linear-Signature` HMAC verification).
263
+
253
264
  [Complete documentation →](docs/guides/CONFIGURATION.md)
254
265
  </details>
255
266
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsErrorDashboard
4
- # Receives webhooks from GitHub/GitLab/Codeberg for two-way issue sync.
4
+ # Receives webhooks from GitHub/GitLab/Codeberg/Linear for two-way issue sync.
5
5
  #
6
6
  # When an issue is closed/reopened on the platform, the corresponding
7
7
  # error in the dashboard is resolved/reopened to match.
@@ -10,6 +10,7 @@ module RailsErrorDashboard
10
10
  # - GitHub: X-Hub-Signature-256 (HMAC-SHA256)
11
11
  # - GitLab: X-Gitlab-Token (shared secret)
12
12
  # - Codeberg: X-Gitea-Signature (HMAC-SHA256)
13
+ # - Linear: Linear-Signature (HMAC-SHA256)
13
14
  class WebhooksController < ActionController::Base
14
15
  skip_before_action :verify_authenticity_token
15
16
 
@@ -29,6 +30,8 @@ module RailsErrorDashboard
29
30
  handle_gitlab(payload)
30
31
  when "codeberg"
31
32
  handle_codeberg(payload)
33
+ when "linear"
34
+ handle_linear(payload)
32
35
  else
33
36
  head :not_found
34
37
  return
@@ -69,6 +72,8 @@ module RailsErrorDashboard
69
72
  verify_gitlab_token(secret)
70
73
  when "codeberg"
71
74
  verify_codeberg_signature(body, secret)
75
+ when "linear"
76
+ verify_linear_signature(body, secret)
72
77
  else
73
78
  false
74
79
  end
@@ -99,6 +104,14 @@ module RailsErrorDashboard
99
104
  ActiveSupport::SecurityUtils.secure_compare(expected, signature)
100
105
  end
101
106
 
107
+ def verify_linear_signature(body, secret)
108
+ signature = request.headers["Linear-Signature"]
109
+ return false unless signature
110
+
111
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
112
+ ActiveSupport::SecurityUtils.secure_compare(expected, signature)
113
+ end
114
+
102
115
  def parse_payload
103
116
  JSON.parse(request.body.read)
104
117
  rescue JSON::ParserError
@@ -162,6 +175,27 @@ module RailsErrorDashboard
162
175
  end
163
176
  end
164
177
 
178
+ # Linear: Issue webhook fires with action create/update/remove. State changes
179
+ # arrive as updates with the old stateId in updatedFrom. Linear has no
180
+ # open/closed binary — completed/canceled state types map to resolved.
181
+ def handle_linear(payload)
182
+ return unless payload["type"] == "Issue" && payload["action"] == "update"
183
+ return unless payload["updatedFrom"]&.key?("stateId")
184
+
185
+ issue_number = payload.dig("data", "number")
186
+ return unless issue_number
187
+
188
+ error = find_error_by_issue(issue_number, "linear")
189
+ return unless error
190
+
191
+ state_type = payload.dig("data", "state", "type")
192
+ if state_type.in?(%w[completed canceled])
193
+ resolve_error(error, "Completed on Linear by #{payload.dig("actor", "name")}")
194
+ elsif state_type.in?(%w[triage backlog unstarted started])
195
+ reopen_error(error)
196
+ end
197
+ end
198
+
165
199
  def find_error_by_issue(issue_number, provider)
166
200
  ErrorLog.find_by(
167
201
  external_issue_number: issue_number,
@@ -18,6 +18,7 @@
18
18
  when "github" then "bi-github"
19
19
  when "gitlab" then "bi-gitlab"
20
20
  when "codeberg" then "bi-git"
21
+ when "linear" then "bi-kanban"
21
22
  else "bi-link-45deg"
22
23
  end %>
23
24
  <% issue = @platform_issue %>
@@ -519,7 +519,7 @@ RailsErrorDashboard.configure do |config|
519
519
  # Safe to enable on production OTel pipelines.
520
520
 
521
521
  # ============================================================================
522
- # ISSUE TRACKING (GitHub / GitLab / Codeberg)
522
+ # ISSUE TRACKING (GitHub / GitLab / Codeberg / Linear)
523
523
  # ============================================================================
524
524
  #
525
525
  # One switch enables everything: issue creation, auto-create on first
@@ -534,7 +534,7 @@ RailsErrorDashboard.configure do |config|
534
534
  # - Priority → labels from platform (with colors)
535
535
  # - Snooze and Mute remain (no platform equivalent)
536
536
  #
537
- # Setup:
537
+ # Setup (GitHub/GitLab/Codeberg):
538
538
  # 1. Create a RED bot account on GitHub/GitLab/Codeberg
539
539
  # 2. Generate a token and set RED_BOT_TOKEN env var
540
540
  # 3. Set git_repository_url above (already used for source code linking)
@@ -543,6 +543,17 @@ RailsErrorDashboard.configure do |config|
543
543
  # config.enable_issue_tracking = true
544
544
  # config.issue_tracker_token = ENV["RED_BOT_TOKEN"]
545
545
  #
546
+ # Setup (Linear):
547
+ # Linear is not a git forge, so it cannot be auto-detected from
548
+ # git_repository_url — set provider and team key explicitly. Issues are
549
+ # created in the team matching the key (e.g. "ENG" for ENG-123 issues).
550
+ # Generate a personal API key under Settings > Security & access.
551
+ #
552
+ # config.enable_issue_tracking = true
553
+ # config.issue_tracker_provider = :linear
554
+ # config.issue_tracker_repo = "ENG" # Linear team key
555
+ # config.issue_tracker_token = ENV["RED_BOT_TOKEN"] # lin_api_... key
556
+ #
546
557
  # Optional overrides:
547
558
  # config.issue_tracker_labels = ["bug"] # Labels added to new issues
548
559
  # config.issue_tracker_auto_create_severities = [:critical, :high] # Auto-create threshold
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RailsErrorDashboard
4
4
  module Commands
5
- # Command: Create an issue on the configured issue tracker (GitHub/GitLab/Codeberg)
5
+ # Command: Create an issue on the configured issue tracker (GitHub/GitLab/Codeberg/Linear)
6
6
  #
7
7
  # Creates the issue via the provider API, then stores the issue URL, number,
8
8
  # and provider on the error record for linking.
@@ -14,7 +14,9 @@ module RailsErrorDashboard
14
14
  PROVIDER_PATTERNS = {
15
15
  github: %r{github\.com/([^/]+/[^/]+)/issues/(\d+)}i,
16
16
  gitlab: %r{gitlab\.com/([^/]+/[^/]+)/-/issues/(\d+)}i,
17
- codeberg: %r{codeberg\.org/([^/]+/[^/]+)/issues/(\d+)}i
17
+ codeberg: %r{codeberg\.org/([^/]+/[^/]+)/issues/(\d+)}i,
18
+ # https://linear.app/<workspace>/issue/ENG-123/<slug> — capture team key + number
19
+ linear: %r{linear\.app/[^/]+/issue/([A-Za-z][A-Za-z0-9]*)-(\d+)}i
18
20
  }.freeze
19
21
 
20
22
  def self.call(error_id, issue_url:)
@@ -92,8 +92,8 @@ module RailsErrorDashboard
92
92
  # issue_webhook_secret is set.
93
93
  attr_accessor :enable_issue_tracking # Master switch (default: false) — enables all platform integration
94
94
  attr_accessor :issue_tracker_token # String or lambda/proc for Rails credentials
95
- attr_accessor :issue_tracker_provider # :github, :gitlab, :codeberg (auto-detected from git_repository_url)
96
- attr_accessor :issue_tracker_repo # "owner/repo" (auto-extracted from git_repository_url)
95
+ attr_accessor :issue_tracker_provider # :github, :gitlab, :codeberg (auto-detected from git_repository_url), or :linear (explicit only)
96
+ attr_accessor :issue_tracker_repo # "owner/repo" (auto-extracted from git_repository_url), or Linear team key like "ENG"
97
97
  attr_accessor :issue_tracker_labels # Array of label strings (default: ["bug"])
98
98
  attr_accessor :issue_tracker_api_url # Custom API base URL for self-hosted instances
99
99
  attr_accessor :issue_tracker_auto_create_severities # Auto-create for these severities (default: [:critical, :high])
@@ -604,7 +604,8 @@ module RailsErrorDashboard
604
604
 
605
605
  if enable_issue_tracking && effective_issue_tracker_provider.nil?
606
606
  warnings << "enable_issue_tracking is true but provider could not be detected. " \
607
- "Set issue_tracker_provider or git_repository_url."
607
+ "Set issue_tracker_provider (:github, :gitlab, :codeberg, :linear) or git_repository_url. " \
608
+ "Note: :linear is never auto-detected — set it explicitly with issue_tracker_repo as the team key."
608
609
  end
609
610
  end
610
611
 
@@ -736,9 +737,11 @@ module RailsErrorDashboard
736
737
  default || blank
737
738
  end
738
739
 
739
- # Resolve the effective issue tracker provider (auto-detect from git_repository_url)
740
+ # Resolve the effective issue tracker provider (auto-detect from git_repository_url).
741
+ # Linear is never auto-detected (it is not a git forge) — set issue_tracker_provider
742
+ # explicitly.
740
743
  #
741
- # @return [Symbol, nil] :github, :gitlab, :codeberg, or nil
744
+ # @return [Symbol, nil] :github, :gitlab, :codeberg, :linear, or nil
742
745
  def effective_issue_tracker_provider
743
746
  return issue_tracker_provider&.to_sym if issue_tracker_provider.present?
744
747
  return nil if git_repository_url.blank?
@@ -751,11 +754,13 @@ module RailsErrorDashboard
751
754
  end
752
755
  end
753
756
 
754
- # Resolve the effective issue tracker repository ("owner/repo")
757
+ # Resolve the effective issue tracker repository ("owner/repo", or Linear team key)
755
758
  #
756
- # @return [String, nil] "owner/repo" or nil
759
+ # @return [String, nil] "owner/repo", Linear team key, or nil
757
760
  def effective_issue_tracker_repo
758
761
  return issue_tracker_repo if issue_tracker_repo.present?
762
+ # A git URL can never yield a Linear team key — require explicit config
763
+ return nil if effective_issue_tracker_provider == :linear
759
764
  return nil if git_repository_url.blank?
760
765
 
761
766
  # Extract owner/repo from URL: https://github.com/owner/repo(.git)
@@ -783,6 +788,7 @@ module RailsErrorDashboard
783
788
  when :github then "https://api.github.com"
784
789
  when :gitlab then "https://gitlab.com/api/v4"
785
790
  when :codeberg then "https://codeberg.org/api/v1"
791
+ when :linear then "https://api.linear.app/graphql"
786
792
  end
787
793
  end
788
794
 
@@ -8,8 +8,9 @@ module RailsErrorDashboard
8
8
  module Services
9
9
  # Base class and factory for issue tracker API clients.
10
10
  #
11
- # Supports GitHub, GitLab, and Codeberg/Gitea/Forgejo via a unified interface.
12
- # Each provider implements the same methods with provider-specific API calls.
11
+ # Supports GitHub, GitLab, Codeberg/Gitea/Forgejo, and Linear via a unified
12
+ # interface. Each provider implements the same methods with provider-specific
13
+ # API calls.
13
14
  #
14
15
  # @example
15
16
  # client = IssueTrackerClient.for(:github, token: "ghp_xxx", repo: "user/repo")
@@ -23,9 +24,9 @@ module RailsErrorDashboard
23
24
 
24
25
  # Factory method — returns the correct client for the provider
25
26
  #
26
- # @param provider [Symbol] :github, :gitlab, or :codeberg
27
+ # @param provider [Symbol] :github, :gitlab, :codeberg, or :linear
27
28
  # @param token [String] API authentication token
28
- # @param repo [String] Repository identifier ("owner/repo")
29
+ # @param repo [String] Repository identifier ("owner/repo"), or Linear team key ("ENG")
29
30
  # @param api_url [String, nil] Custom API base URL (for self-hosted)
30
31
  # @return [IssueTrackerClient] Provider-specific client instance
31
32
  def self.for(provider, token:, repo:, api_url: nil)
@@ -36,8 +37,10 @@ module RailsErrorDashboard
36
37
  GitLabIssueClient.new(token: token, repo: repo, api_url: api_url)
37
38
  when :codeberg
38
39
  CodebergIssueClient.new(token: token, repo: repo, api_url: api_url)
40
+ when :linear
41
+ LinearIssueClient.new(token: token, repo: repo, api_url: api_url)
39
42
  else
40
- raise ArgumentError, "Unknown issue tracker provider: #{provider}. Supported: :github, :gitlab, :codeberg"
43
+ raise ArgumentError, "Unknown issue tracker provider: #{provider}. Supported: :github, :gitlab, :codeberg, :linear"
41
44
  end
42
45
  end
43
46
 
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ # Linear GraphQL API client for issue management.
6
+ #
7
+ # API Docs: https://developers.linear.app/docs/graphql/working-with-the-graphql-api
8
+ # Auth: Personal API key (Settings > Security & access > Personal API keys)
9
+ # Rate limit: 1,500 requests/hour per API key
10
+ #
11
+ # Unlike the git forges, Linear has no "owner/repo" — issues belong to a
12
+ # team. The `repo` argument holds the team key (e.g. "ENG"), and issues
13
+ # are addressed by their human identifier ("ENG-123"), reconstructed from
14
+ # the team key plus the team-scoped issue number we store.
15
+ #
16
+ # Linear also has no open/closed binary — issues move between typed
17
+ # workflow states. Closing maps to the team's first `completed`-type
18
+ # state, reopening to the first `unstarted` (or `backlog`) state.
19
+ class LinearIssueClient < IssueTrackerClient
20
+ REOPEN_STATE_TYPES = [ "unstarted", "backlog", "triage" ].freeze
21
+
22
+ def initialize(token:, repo:, api_url: nil)
23
+ super
24
+ @api_url = api_url || "https://api.linear.app/graphql"
25
+ end
26
+
27
+ def create_issue(title:, body:, labels: [])
28
+ return error_response(@last_error || "Linear team '#{@repo}' not found") unless team_id
29
+
30
+ input = { teamId: team_id, title: title, description: truncate_body(body) }
31
+ label_ids = resolve_label_ids(labels)
32
+ input[:labelIds] = label_ids if label_ids.any?
33
+
34
+ data = graphql(<<~GRAPHQL, input: input)
35
+ mutation($input: IssueCreateInput!) {
36
+ issueCreate(input: $input) { success issue { identifier number url } }
37
+ }
38
+ GRAPHQL
39
+
40
+ issue = data&.dig("issueCreate", "issue")
41
+ if issue
42
+ success_response(url: issue["url"], number: issue["number"])
43
+ else
44
+ error_response(@last_error || "Linear API error: issue creation failed")
45
+ end
46
+ end
47
+
48
+ def close_issue(number:)
49
+ update_issue_state(number, "completed")
50
+ end
51
+
52
+ def reopen_issue(number:)
53
+ update_issue_state(number, REOPEN_STATE_TYPES)
54
+ end
55
+
56
+ def add_comment(number:, body:)
57
+ issue_id = find_issue_id(number)
58
+ return error_response(@last_error || "Linear issue #{identifier_for(number)} not found") unless issue_id
59
+
60
+ data = graphql(<<~GRAPHQL, input: { issueId: issue_id, body: truncate_body(body) })
61
+ mutation($input: CommentCreateInput!) {
62
+ commentCreate(input: $input) { success comment { url } }
63
+ }
64
+ GRAPHQL
65
+
66
+ comment = data&.dig("commentCreate", "comment")
67
+ comment ? success_response(url: comment["url"]) : error_response(@last_error || "Linear API error: comment failed")
68
+ end
69
+
70
+ def fetch_comments(number:, per_page: 10)
71
+ data = graphql(<<~GRAPHQL, id: identifier_for(number), first: per_page)
72
+ query($id: String!, $first: Int!) {
73
+ issue(id: $id) {
74
+ comments(first: $first) {
75
+ nodes { body createdAt url user { name avatarUrl } }
76
+ }
77
+ }
78
+ }
79
+ GRAPHQL
80
+
81
+ nodes = data&.dig("issue", "comments", "nodes")
82
+ return error_response(@last_error || "Linear API error: could not fetch comments") unless nodes
83
+
84
+ comments = nodes.map { |c|
85
+ {
86
+ author: c.dig("user", "name"),
87
+ avatar_url: c.dig("user", "avatarUrl"),
88
+ body: c["body"],
89
+ created_at: c["createdAt"],
90
+ url: c["url"]
91
+ }
92
+ }
93
+ success_response(comments: comments)
94
+ end
95
+
96
+ def fetch_issue(number:)
97
+ data = graphql(<<~GRAPHQL, id: identifier_for(number))
98
+ query($id: String!) {
99
+ issue(id: $id) {
100
+ title
101
+ state { name type }
102
+ assignee { name avatarUrl }
103
+ labels { nodes { name color } }
104
+ }
105
+ }
106
+ GRAPHQL
107
+
108
+ issue = data&.dig("issue")
109
+ return error_response(@last_error || "Linear API error: could not fetch issue") unless issue
110
+
111
+ assignee = issue["assignee"]
112
+ success_response(
113
+ state: closed_state_type?(issue.dig("state", "type")) ? "closed" : "open",
114
+ title: issue["title"],
115
+ assignees: assignee ? [ { login: assignee["name"], avatar_url: assignee["avatarUrl"] } ] : [],
116
+ labels: (issue.dig("labels", "nodes") || []).map { |l|
117
+ { name: l["name"], color: l["color"]&.delete("#") }
118
+ }
119
+ )
120
+ end
121
+
122
+ private
123
+
124
+ # Linear issues are addressed by "TEAM-123" — team key + stored number
125
+ def identifier_for(number)
126
+ "#{@repo}-#{number}"
127
+ end
128
+
129
+ def closed_state_type?(state_type)
130
+ [ "completed", "canceled" ].include?(state_type)
131
+ end
132
+
133
+ def update_issue_state(number, state_types)
134
+ issue_id = find_issue_id(number)
135
+ return error_response(@last_error || "Linear issue #{identifier_for(number)} not found") unless issue_id
136
+
137
+ state_id = workflow_state_id(Array(state_types))
138
+ return error_response(@last_error || "No matching workflow state for #{Array(state_types).join('/')}") unless state_id
139
+
140
+ data = graphql(<<~GRAPHQL, id: issue_id, input: { stateId: state_id })
141
+ mutation($id: String!, $input: IssueUpdateInput!) {
142
+ issueUpdate(id: $id, input: $input) { success }
143
+ }
144
+ GRAPHQL
145
+
146
+ data&.dig("issueUpdate", "success") ? success_response({}) : error_response(@last_error || "Linear API error: state update failed")
147
+ end
148
+
149
+ def find_issue_id(number)
150
+ data = graphql("query($id: String!) { issue(id: $id) { id } }", id: identifier_for(number))
151
+ data&.dig("issue", "id")
152
+ end
153
+
154
+ def team_id
155
+ @team_id ||= begin
156
+ data = graphql(<<~GRAPHQL, key: @repo)
157
+ query($key: String!) {
158
+ teams(filter: { key: { eq: $key } }) { nodes { id } }
159
+ }
160
+ GRAPHQL
161
+ data&.dig("teams", "nodes", 0, "id")
162
+ end
163
+ end
164
+
165
+ # Pick the first workflow state whose type matches, in preference order
166
+ def workflow_state_id(preferred_types)
167
+ states = workflow_states
168
+ return nil unless states
169
+
170
+ preferred_types.each do |type|
171
+ match = states.find { |s| s["type"] == type }
172
+ return match["id"] if match
173
+ end
174
+ nil
175
+ end
176
+
177
+ def workflow_states
178
+ @workflow_states ||= begin
179
+ data = graphql(<<~GRAPHQL, key: @repo)
180
+ query($key: String!) {
181
+ workflowStates(filter: { team: { key: { eq: $key } } }) {
182
+ nodes { id name type position }
183
+ }
184
+ }
185
+ GRAPHQL
186
+ data&.dig("workflowStates", "nodes")&.sort_by { |s| s["position"].to_f }
187
+ end
188
+ end
189
+
190
+ # Best-effort: resolve label names to UUIDs, creating missing ones.
191
+ # Label failures must never block issue creation.
192
+ def resolve_label_ids(names)
193
+ names = Array(names).map(&:to_s).reject(&:empty?)
194
+ return [] if names.empty?
195
+
196
+ data = graphql(<<~GRAPHQL, names: names)
197
+ query($names: [String!]) {
198
+ issueLabels(filter: { name: { in: $names } }) { nodes { id name } }
199
+ }
200
+ GRAPHQL
201
+ existing = data&.dig("issueLabels", "nodes") || []
202
+ ids = existing.map { |l| l["id"] }
203
+
204
+ missing = names - existing.map { |l| l["name"] }
205
+ missing.each do |name|
206
+ created = graphql(<<~GRAPHQL, input: { name: name, teamId: team_id })
207
+ mutation($input: IssueLabelCreateInput!) {
208
+ issueLabelCreate(input: $input) { issueLabel { id } }
209
+ }
210
+ GRAPHQL
211
+ id = created&.dig("issueLabelCreate", "issueLabel", "id")
212
+ ids << id if id
213
+ end
214
+
215
+ ids
216
+ rescue
217
+ []
218
+ end
219
+
220
+ # Execute a GraphQL request. Returns the "data" hash, or nil on any
221
+ # error (with the message stashed in @last_error for the caller).
222
+ def graphql(query, variables = {})
223
+ @last_error = nil
224
+ response = http_post(@api_url, { query: query, variables: variables }, auth_headers)
225
+
226
+ if response[:status] != 200
227
+ message = response.dig(:body, "errors", 0, "message") || response[:error]
228
+ @last_error = "Linear API error (#{response[:status]}): #{message}"
229
+ return nil
230
+ end
231
+
232
+ errors = response.dig(:body, "errors")
233
+ if errors.present?
234
+ @last_error = "Linear API error: #{errors.first["message"]}"
235
+ return nil
236
+ end
237
+
238
+ response.dig(:body, "data")
239
+ end
240
+
241
+ def auth_headers
242
+ # Personal API keys are passed bare; OAuth tokens need a Bearer prefix
243
+ value = @token.to_s.start_with?("lin_api_") ? @token : "Bearer #{@token}"
244
+ { "Authorization" => value }
245
+ end
246
+ end
247
+ end
248
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.1"
3
3
  end
@@ -68,6 +68,7 @@ require "rails_error_dashboard/services/issue_tracker_client"
68
68
  require "rails_error_dashboard/services/github_issue_client"
69
69
  require "rails_error_dashboard/services/gitlab_issue_client"
70
70
  require "rails_error_dashboard/services/codeberg_issue_client"
71
+ require "rails_error_dashboard/services/linear_issue_client"
71
72
  require "rails_error_dashboard/services/database_health_inspector"
72
73
  require "rails_error_dashboard/services/cache_analyzer"
73
74
  require "rails_error_dashboard/services/llm_summary"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -463,6 +463,7 @@ files:
463
463
  - lib/rails_error_dashboard/services/gitlab_issue_client.rb
464
464
  - lib/rails_error_dashboard/services/issue_body_formatter.rb
465
465
  - lib/rails_error_dashboard/services/issue_tracker_client.rb
466
+ - lib/rails_error_dashboard/services/linear_issue_client.rb
466
467
  - lib/rails_error_dashboard/services/llm_client.rb
467
468
  - lib/rails_error_dashboard/services/llm_cost_estimator.rb
468
469
  - lib/rails_error_dashboard/services/llm_summary.rb
@@ -511,7 +512,7 @@ metadata:
511
512
  funding_uri: https://github.com/sponsors/AnjanJ
512
513
  post_install_message: |
513
514
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
514
- RED (Rails Error Dashboard) v0.8.0
515
+ RED (Rails Error Dashboard) v0.8.1
515
516
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
516
517
 
517
518
  First install:
@@ -547,7 +548,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
547
548
  - !ruby/object:Gem::Version
548
549
  version: '0'
549
550
  requirements: []
550
- rubygems_version: 4.0.3
551
+ rubygems_version: 3.6.9
551
552
  specification_version: 4
552
553
  summary: Self-hosted error tracking and exception monitoring for Rails. Free, forever.
553
554
  test_files: []