fastlane-plugin-sentry_api 0.1.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.
@@ -0,0 +1,283 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/sentry_api_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ module SharedValues
7
+ SENTRY_CRASH_FREE_SESSION_RATE = :SENTRY_CRASH_FREE_SESSION_RATE
8
+ SENTRY_CRASH_FREE_USER_RATE = :SENTRY_CRASH_FREE_USER_RATE
9
+ SENTRY_TOTAL_SESSIONS = :SENTRY_TOTAL_SESSIONS
10
+ SENTRY_TOTAL_USERS = :SENTRY_TOTAL_USERS
11
+ SENTRY_SESSION_GROUPS = :SENTRY_SESSION_GROUPS
12
+ SENTRY_CRASH_FREE_SESSIONS_STATUS_CODE = :SENTRY_CRASH_FREE_SESSIONS_STATUS_CODE
13
+ SENTRY_CRASH_FREE_SESSIONS_JSON = :SENTRY_CRASH_FREE_SESSIONS_JSON
14
+ end
15
+
16
+ # Query crash-free session and user rates from the Sentry Sessions API.
17
+ # Supports aggregate metrics (for week-over-week) and grouped-by-release (for release-over-release).
18
+ class SentryCrashFreeSessionsAction < Action
19
+ class << self
20
+ def run(params)
21
+ auth_token = params[:auth_token]
22
+ org_slug = params[:org_slug]
23
+ project_id = params[:project_id]
24
+
25
+ query_params = build_query_params(params, project_id)
26
+
27
+ UI.message("Fetching crash-free session metrics from Sentry (#{query_params[:statsPeriod] || 'custom range'})...")
28
+
29
+ response = Helper::SentryApiHelper.get_sessions(
30
+ auth_token: auth_token,
31
+ org_slug: org_slug,
32
+ params: query_params
33
+ )
34
+
35
+ status_code = response[:status]
36
+ json = response[:json]
37
+
38
+ unless status_code.between?(200, 299)
39
+ UI.user_error!("Sentry Sessions API error #{status_code}: #{response[:body]}")
40
+ return nil
41
+ end
42
+
43
+ result = parse_response(json, params[:group_by])
44
+
45
+ # Store in lane context
46
+ Actions.lane_context[SharedValues::SENTRY_CRASH_FREE_SESSIONS_STATUS_CODE] = status_code
47
+ Actions.lane_context[SharedValues::SENTRY_CRASH_FREE_SESSIONS_JSON] = json
48
+ Actions.lane_context[SharedValues::SENTRY_CRASH_FREE_SESSION_RATE] = result[:crash_free_session_rate]
49
+ Actions.lane_context[SharedValues::SENTRY_CRASH_FREE_USER_RATE] = result[:crash_free_user_rate]
50
+ Actions.lane_context[SharedValues::SENTRY_TOTAL_SESSIONS] = result[:total_sessions]
51
+ Actions.lane_context[SharedValues::SENTRY_TOTAL_USERS] = result[:total_users]
52
+ Actions.lane_context[SharedValues::SENTRY_SESSION_GROUPS] = result[:groups]
53
+
54
+ # Log results
55
+ if result[:groups] && !result[:groups].empty?
56
+ UI.success("Fetched #{result[:groups].length} session groups")
57
+ result[:groups].each do |group|
58
+ label = group[:by].values.first || "aggregate"
59
+ UI.message(" #{label}: sessions=#{format_pct(group[:crash_free_session_rate])} users=#{format_pct(group[:crash_free_user_rate])}")
60
+ end
61
+ else
62
+ UI.success("Crash-free sessions: #{format_pct(result[:crash_free_session_rate])}")
63
+ UI.success("Crash-free users: #{format_pct(result[:crash_free_user_rate])}")
64
+ UI.success("Total sessions: #{result[:total_sessions]}, Total users: #{result[:total_users]}")
65
+ end
66
+
67
+ result
68
+ end
69
+
70
+ #####################################################
71
+ # @!group Documentation
72
+ #####################################################
73
+
74
+ def description
75
+ "Query crash-free session and user rates from the Sentry Sessions API"
76
+ end
77
+
78
+ def details
79
+ [
80
+ "Queries the Sentry Sessions API for crash-free session rates, crash-free user rates,",
81
+ "total sessions, and total users. Supports aggregate metrics for a time period",
82
+ "(useful for week-over-week comparison) and grouped-by-release metrics",
83
+ "(useful for release-over-release comparison).",
84
+ "",
85
+ "Use `stats_period` for rolling windows (e.g. '7d', '14d', '30d'),",
86
+ "or `start_date` + `end_date` for a specific date range.",
87
+ "",
88
+ "API Documentation: https://docs.sentry.io/api/releases/retrieve-release-health-session-statistics/"
89
+ ].join("\n")
90
+ end
91
+
92
+ def available_options
93
+ [
94
+ FastlaneCore::ConfigItem.new(key: :auth_token,
95
+ env_name: "SENTRY_AUTH_TOKEN",
96
+ description: "Sentry API Bearer auth token",
97
+ optional: false,
98
+ type: String,
99
+ sensitive: true,
100
+ code_gen_sensitive: true,
101
+ verify_block: proc do |value|
102
+ UI.user_error!("No Sentry auth token given, pass using `auth_token: 'token'`") if value.to_s.empty?
103
+ end),
104
+ FastlaneCore::ConfigItem.new(key: :org_slug,
105
+ env_name: "SENTRY_ORG_SLUG",
106
+ description: "Sentry organization slug",
107
+ optional: false,
108
+ type: String,
109
+ verify_block: proc do |value|
110
+ UI.user_error!("No Sentry org slug given, pass using `org_slug: 'my-org'`") if value.to_s.empty?
111
+ end),
112
+ FastlaneCore::ConfigItem.new(key: :project_id,
113
+ env_name: "SENTRY_PROJECT_ID",
114
+ description: "Sentry numeric project ID",
115
+ optional: false,
116
+ type: String,
117
+ verify_block: proc do |value|
118
+ UI.user_error!("No Sentry project ID given, pass using `project_id: '12345'`") if value.to_s.empty?
119
+ end),
120
+ FastlaneCore::ConfigItem.new(key: :environment,
121
+ env_name: "SENTRY_ENVIRONMENT",
122
+ description: "Environment filter (e.g. 'production')",
123
+ optional: true,
124
+ default_value: "production",
125
+ type: String),
126
+ FastlaneCore::ConfigItem.new(key: :stats_period,
127
+ description: "Rolling time window (e.g. '7d', '14d', '30d'). Mutually exclusive with start_date/end_date",
128
+ optional: true,
129
+ default_value: "7d",
130
+ type: String),
131
+ FastlaneCore::ConfigItem.new(key: :start_date,
132
+ description: "Start date in ISO 8601 format (e.g. '2026-03-01T00:00:00Z'). Use with end_date instead of stats_period",
133
+ optional: true,
134
+ type: String),
135
+ FastlaneCore::ConfigItem.new(key: :end_date,
136
+ description: "End date in ISO 8601 format (e.g. '2026-03-08T00:00:00Z'). Use with start_date instead of stats_period",
137
+ optional: true,
138
+ type: String),
139
+ FastlaneCore::ConfigItem.new(key: :group_by,
140
+ description: "Group results by dimension: 'release', 'environment', or nil for aggregate",
141
+ optional: true,
142
+ type: String),
143
+ FastlaneCore::ConfigItem.new(key: :per_page,
144
+ description: "Number of groups to return when group_by is set (max 100)",
145
+ optional: true,
146
+ default_value: 10,
147
+ type: Integer),
148
+ FastlaneCore::ConfigItem.new(key: :order_by,
149
+ description: "Sort order for grouped results (e.g. '-sum(session)', '-crash_free_rate(session)')",
150
+ optional: true,
151
+ default_value: "-sum(session)",
152
+ type: String)
153
+ ]
154
+ end
155
+
156
+ def output
157
+ [
158
+ ['SENTRY_CRASH_FREE_SESSION_RATE', 'Crash-free session rate as a float (e.g. 0.9974)'],
159
+ ['SENTRY_CRASH_FREE_USER_RATE', 'Crash-free user rate as a float (e.g. 0.9981)'],
160
+ ['SENTRY_TOTAL_SESSIONS', 'Total number of sessions in the period'],
161
+ ['SENTRY_TOTAL_USERS', 'Total number of unique users in the period'],
162
+ ['SENTRY_SESSION_GROUPS', 'Array of group hashes when group_by is set'],
163
+ ['SENTRY_CRASH_FREE_SESSIONS_STATUS_CODE', 'HTTP status code from the Sentry API'],
164
+ ['SENTRY_CRASH_FREE_SESSIONS_JSON', 'Raw JSON response from the Sentry API']
165
+ ]
166
+ end
167
+
168
+ def return_value
169
+ "A hash with :crash_free_session_rate, :crash_free_user_rate, :total_sessions, :total_users, and :groups (when group_by is set)."
170
+ end
171
+
172
+ def authors
173
+ ["crazymanish"]
174
+ end
175
+
176
+ def example_code
177
+ [
178
+ '# Aggregate crash-free rate for last 7 days
179
+ sentry_crash_free_sessions(stats_period: "7d")
180
+ rate = lane_context[SharedValues::SENTRY_CRASH_FREE_SESSION_RATE]
181
+ UI.message("Crash-free sessions: #{(rate * 100).round(2)}%")',
182
+
183
+ '# Grouped by release (release-over-release comparison)
184
+ result = sentry_crash_free_sessions(stats_period: "30d", group_by: "release", per_page: 5)
185
+ result[:groups].each do |g|
186
+ puts "#{g[:by]["release"]}: #{(g[:crash_free_session_rate] * 100).round(2)}%"
187
+ end',
188
+
189
+ '# Custom date range (previous week for week-over-week)
190
+ sentry_crash_free_sessions(start_date: "2026-02-24T00:00:00Z", end_date: "2026-03-03T00:00:00Z")'
191
+ ]
192
+ end
193
+
194
+ def category
195
+ :misc
196
+ end
197
+
198
+ def is_supported?(platform)
199
+ true
200
+ end
201
+
202
+ private
203
+
204
+ def build_query_params(params, project_id)
205
+ fields = [
206
+ 'crash_free_rate(session)',
207
+ 'crash_free_rate(user)',
208
+ 'sum(session)',
209
+ 'count_unique(user)'
210
+ ]
211
+
212
+ query_params = {
213
+ field: fields,
214
+ project: project_id.to_s,
215
+ includeSeries: '0'
216
+ }
217
+
218
+ # Time range: stats_period OR start/end
219
+ if params[:start_date] && params[:end_date]
220
+ query_params[:start] = params[:start_date]
221
+ query_params[:end] = params[:end_date]
222
+ else
223
+ query_params[:statsPeriod] = params[:stats_period] || '7d'
224
+ end
225
+
226
+ query_params[:environment] = params[:environment] if params[:environment]
227
+
228
+ # Grouping
229
+ if params[:group_by]
230
+ query_params[:groupBy] = params[:group_by]
231
+ query_params[:per_page] = params[:per_page].to_s if params[:per_page]
232
+ query_params[:orderBy] = params[:order_by] if params[:order_by]
233
+ end
234
+
235
+ query_params
236
+ end
237
+
238
+ def parse_response(json, group_by)
239
+ groups_data = json&.dig('groups') || []
240
+
241
+ if group_by && !group_by.empty?
242
+ # Grouped response: return array of group results
243
+ groups = groups_data.map do |group|
244
+ totals = group['totals'] || {}
245
+ {
246
+ by: group['by'] || {},
247
+ crash_free_session_rate: totals['crash_free_rate(session)'],
248
+ crash_free_user_rate: totals['crash_free_rate(user)'],
249
+ total_sessions: totals['sum(session)'],
250
+ total_users: totals['count_unique(user)']
251
+ }
252
+ end
253
+
254
+ {
255
+ crash_free_session_rate: nil,
256
+ crash_free_user_rate: nil,
257
+ total_sessions: nil,
258
+ total_users: nil,
259
+ groups: groups
260
+ }
261
+ else
262
+ # Aggregate response: single group
263
+ totals = groups_data.dig(0, 'totals') || {}
264
+
265
+ {
266
+ crash_free_session_rate: totals['crash_free_rate(session)'],
267
+ crash_free_user_rate: totals['crash_free_rate(user)'],
268
+ total_sessions: totals['sum(session)'],
269
+ total_users: totals['count_unique(user)'],
270
+ groups: nil
271
+ }
272
+ end
273
+ end
274
+
275
+ def format_pct(value)
276
+ return "N/A" if value.nil?
277
+
278
+ "#{(value * 100).round(4)}%"
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,198 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/sentry_api_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ module SharedValues
7
+ SENTRY_CRASH_FREE_USER_RATE_ONLY = :SENTRY_CRASH_FREE_USER_RATE_ONLY
8
+ SENTRY_TOTAL_USERS_ONLY = :SENTRY_TOTAL_USERS_ONLY
9
+ SENTRY_CRASH_FREE_USERS_STATUS_CODE = :SENTRY_CRASH_FREE_USERS_STATUS_CODE
10
+ SENTRY_CRASH_FREE_USERS_JSON = :SENTRY_CRASH_FREE_USERS_JSON
11
+ end
12
+
13
+ # Convenience action focused on user-centric crash-free metrics.
14
+ # Queries the same Sentry Sessions API but returns user-focused results.
15
+ class SentryCrashFreeUsersAction < Action
16
+ class << self
17
+ def run(params)
18
+ auth_token = params[:auth_token]
19
+ org_slug = params[:org_slug]
20
+ project_id = params[:project_id]
21
+
22
+ query_params = build_query_params(params, project_id)
23
+
24
+ UI.message("Fetching crash-free user metrics from Sentry (#{query_params[:statsPeriod] || 'custom range'})...")
25
+
26
+ response = Helper::SentryApiHelper.get_sessions(
27
+ auth_token: auth_token,
28
+ org_slug: org_slug,
29
+ params: query_params
30
+ )
31
+
32
+ status_code = response[:status]
33
+ json = response[:json]
34
+
35
+ unless status_code.between?(200, 299)
36
+ UI.user_error!("Sentry Sessions API error #{status_code}: #{response[:body]}")
37
+ return nil
38
+ end
39
+
40
+ result = parse_response(json)
41
+
42
+ Actions.lane_context[SharedValues::SENTRY_CRASH_FREE_USERS_STATUS_CODE] = status_code
43
+ Actions.lane_context[SharedValues::SENTRY_CRASH_FREE_USERS_JSON] = json
44
+ Actions.lane_context[SharedValues::SENTRY_CRASH_FREE_USER_RATE_ONLY] = result[:crash_free_user_rate]
45
+ Actions.lane_context[SharedValues::SENTRY_TOTAL_USERS_ONLY] = result[:total_users]
46
+
47
+ UI.success("Crash-free users: #{format_pct(result[:crash_free_user_rate])}")
48
+ UI.success("Total unique users: #{result[:total_users]}")
49
+
50
+ result
51
+ end
52
+
53
+ #####################################################
54
+ # @!group Documentation
55
+ #####################################################
56
+
57
+ def description
58
+ "Query crash-free user rate from the Sentry Sessions API"
59
+ end
60
+
61
+ def details
62
+ [
63
+ "Convenience action for fetching user-centric crash-free metrics from Sentry.",
64
+ "Queries the Sessions API for crash_free_rate(user) and count_unique(user).",
65
+ "For full session + user metrics, use sentry_crash_free_sessions instead.",
66
+ "",
67
+ "API Documentation: https://docs.sentry.io/api/releases/retrieve-release-health-session-statistics/"
68
+ ].join("\n")
69
+ end
70
+
71
+ def available_options
72
+ [
73
+ FastlaneCore::ConfigItem.new(key: :auth_token,
74
+ env_name: "SENTRY_AUTH_TOKEN",
75
+ description: "Sentry API Bearer auth token",
76
+ optional: false,
77
+ type: String,
78
+ sensitive: true,
79
+ code_gen_sensitive: true,
80
+ verify_block: proc do |value|
81
+ UI.user_error!("No Sentry auth token given, pass using `auth_token: 'token'`") if value.to_s.empty?
82
+ end),
83
+ FastlaneCore::ConfigItem.new(key: :org_slug,
84
+ env_name: "SENTRY_ORG_SLUG",
85
+ description: "Sentry organization slug",
86
+ optional: false,
87
+ type: String,
88
+ verify_block: proc do |value|
89
+ UI.user_error!("No Sentry org slug given, pass using `org_slug: 'my-org'`") if value.to_s.empty?
90
+ end),
91
+ FastlaneCore::ConfigItem.new(key: :project_id,
92
+ env_name: "SENTRY_PROJECT_ID",
93
+ description: "Sentry numeric project ID",
94
+ optional: false,
95
+ type: String,
96
+ verify_block: proc do |value|
97
+ UI.user_error!("No Sentry project ID given, pass using `project_id: '12345'`") if value.to_s.empty?
98
+ end),
99
+ FastlaneCore::ConfigItem.new(key: :environment,
100
+ env_name: "SENTRY_ENVIRONMENT",
101
+ description: "Environment filter (e.g. 'production')",
102
+ optional: true,
103
+ default_value: "production",
104
+ type: String),
105
+ FastlaneCore::ConfigItem.new(key: :stats_period,
106
+ description: "Rolling time window (e.g. '7d', '14d', '30d')",
107
+ optional: true,
108
+ default_value: "7d",
109
+ type: String),
110
+ FastlaneCore::ConfigItem.new(key: :start_date,
111
+ description: "Start date in ISO 8601 format. Use with end_date instead of stats_period",
112
+ optional: true,
113
+ type: String),
114
+ FastlaneCore::ConfigItem.new(key: :end_date,
115
+ description: "End date in ISO 8601 format. Use with start_date instead of stats_period",
116
+ optional: true,
117
+ type: String)
118
+ ]
119
+ end
120
+
121
+ def output
122
+ [
123
+ ['SENTRY_CRASH_FREE_USER_RATE_ONLY', 'Crash-free user rate as a float (e.g. 0.9991)'],
124
+ ['SENTRY_TOTAL_USERS_ONLY', 'Total unique users in the period'],
125
+ ['SENTRY_CRASH_FREE_USERS_STATUS_CODE', 'HTTP status code from the Sentry API'],
126
+ ['SENTRY_CRASH_FREE_USERS_JSON', 'Raw JSON response from the Sentry API']
127
+ ]
128
+ end
129
+
130
+ def return_value
131
+ "A hash with :crash_free_user_rate and :total_users."
132
+ end
133
+
134
+ def authors
135
+ ["crazymanish"]
136
+ end
137
+
138
+ def example_code
139
+ [
140
+ 'sentry_crash_free_users(stats_period: "7d")
141
+ rate = lane_context[SharedValues::SENTRY_CRASH_FREE_USER_RATE_ONLY]
142
+ UI.message("Crash-free users: #{(rate * 100).round(2)}%")'
143
+ ]
144
+ end
145
+
146
+ def category
147
+ :misc
148
+ end
149
+
150
+ def is_supported?(platform)
151
+ true
152
+ end
153
+
154
+ private
155
+
156
+ def build_query_params(params, project_id)
157
+ fields = [
158
+ 'crash_free_rate(user)',
159
+ 'count_unique(user)'
160
+ ]
161
+
162
+ query_params = {
163
+ field: fields,
164
+ project: project_id.to_s,
165
+ includeSeries: '0'
166
+ }
167
+
168
+ if params[:start_date] && params[:end_date]
169
+ query_params[:start] = params[:start_date]
170
+ query_params[:end] = params[:end_date]
171
+ else
172
+ query_params[:statsPeriod] = params[:stats_period] || '7d'
173
+ end
174
+
175
+ query_params[:environment] = params[:environment] if params[:environment]
176
+
177
+ query_params
178
+ end
179
+
180
+ def parse_response(json)
181
+ groups_data = json&.dig('groups') || []
182
+ totals = groups_data.dig(0, 'totals') || {}
183
+
184
+ {
185
+ crash_free_user_rate: totals['crash_free_rate(user)'],
186
+ total_users: totals['count_unique(user)']
187
+ }
188
+ end
189
+
190
+ def format_pct(value)
191
+ return "N/A" if value.nil?
192
+
193
+ "#{(value * 100).round(4)}%"
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,207 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/sentry_api_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ module SharedValues
7
+ SENTRY_ISSUES = :SENTRY_ISSUES
8
+ SENTRY_ISSUE_COUNT = :SENTRY_ISSUE_COUNT
9
+ SENTRY_ISSUES_STATUS_CODE = :SENTRY_ISSUES_STATUS_CODE
10
+ SENTRY_ISSUES_JSON = :SENTRY_ISSUES_JSON
11
+ end
12
+
13
+ # Fetch issues from a Sentry project. Supports filtering by release, query, sort order, etc.
14
+ # Uses the Projects Issues API: GET /api/0/projects/{org}/{project}/issues/
15
+ class SentryListIssuesAction < Action
16
+ class << self
17
+ def run(params)
18
+ auth_token = params[:auth_token]
19
+ org_slug = params[:org_slug]
20
+ project_slug = params[:project_slug]
21
+
22
+ query_params = build_query_params(params)
23
+
24
+ UI.message("Fetching issues from Sentry project '#{project_slug}'...")
25
+
26
+ response = Helper::SentryApiHelper.get_issues(
27
+ auth_token: auth_token,
28
+ org_slug: org_slug,
29
+ project_slug: project_slug,
30
+ params: query_params
31
+ )
32
+
33
+ status_code = response[:status]
34
+ json = response[:json]
35
+
36
+ unless status_code.between?(200, 299)
37
+ UI.user_error!("Sentry Issues API error #{status_code}: #{response[:body]}")
38
+ return nil
39
+ end
40
+
41
+ issues = parse_response(json)
42
+ issue_count = issues.length
43
+
44
+ Actions.lane_context[SharedValues::SENTRY_ISSUES_STATUS_CODE] = status_code
45
+ Actions.lane_context[SharedValues::SENTRY_ISSUES_JSON] = json
46
+ Actions.lane_context[SharedValues::SENTRY_ISSUES] = issues
47
+ Actions.lane_context[SharedValues::SENTRY_ISSUE_COUNT] = issue_count
48
+
49
+ UI.success("Fetched #{issue_count} issues from '#{project_slug}'")
50
+ issues.first(5).each do |issue|
51
+ UI.message(" #{issue[:short_id]}: #{issue[:title]} (#{issue[:event_count]} events, #{issue[:user_count]} users)")
52
+ end
53
+
54
+ { issues: issues, count: issue_count }
55
+ end
56
+
57
+ #####################################################
58
+ # @!group Documentation
59
+ #####################################################
60
+
61
+ def description
62
+ "Fetch issues from a Sentry project"
63
+ end
64
+
65
+ def details
66
+ [
67
+ "Fetches issues from a Sentry project using the Projects Issues API.",
68
+ "Supports filtering by release version, query string, sort order, and pagination.",
69
+ "Useful for comparing issues across releases (e.g. latest vs previous).",
70
+ "",
71
+ "API Documentation: https://docs.sentry.io/api/events/list-a-projects-issues/"
72
+ ].join("\n")
73
+ end
74
+
75
+ def available_options
76
+ [
77
+ FastlaneCore::ConfigItem.new(key: :auth_token,
78
+ env_name: "SENTRY_AUTH_TOKEN",
79
+ description: "Sentry API Bearer auth token",
80
+ optional: false,
81
+ type: String,
82
+ sensitive: true,
83
+ code_gen_sensitive: true,
84
+ verify_block: proc do |value|
85
+ UI.user_error!("No Sentry auth token given, pass using `auth_token: 'token'`") if value.to_s.empty?
86
+ end),
87
+ FastlaneCore::ConfigItem.new(key: :org_slug,
88
+ env_name: "SENTRY_ORG_SLUG",
89
+ description: "Sentry organization slug",
90
+ optional: false,
91
+ type: String,
92
+ verify_block: proc do |value|
93
+ UI.user_error!("No Sentry org slug given, pass using `org_slug: 'my-org'`") if value.to_s.empty?
94
+ end),
95
+ FastlaneCore::ConfigItem.new(key: :project_slug,
96
+ env_name: "SENTRY_PROJECT_SLUG",
97
+ description: "Sentry project slug (e.g. 'ios', 'android')",
98
+ optional: false,
99
+ type: String,
100
+ verify_block: proc do |value|
101
+ UI.user_error!("No Sentry project slug given, pass using `project_slug: 'ios'`") if value.to_s.empty?
102
+ end),
103
+ FastlaneCore::ConfigItem.new(key: :query,
104
+ description: "Sentry search query (e.g. 'is:unresolved', 'is:unresolved release:v1.0')",
105
+ optional: true,
106
+ default_value: "is:unresolved",
107
+ type: String),
108
+ FastlaneCore::ConfigItem.new(key: :sort,
109
+ description: "Sort order: 'date', 'new', 'freq', 'priority', 'user', 'trend'",
110
+ optional: true,
111
+ default_value: "freq",
112
+ type: String),
113
+ FastlaneCore::ConfigItem.new(key: :stats_period,
114
+ description: "Time window for issue stats (e.g. '7d', '14d', '30d')",
115
+ optional: true,
116
+ type: String),
117
+ FastlaneCore::ConfigItem.new(key: :per_page,
118
+ description: "Number of issues to return (max 100)",
119
+ optional: true,
120
+ default_value: 25,
121
+ type: Integer),
122
+ FastlaneCore::ConfigItem.new(key: :cursor,
123
+ description: "Pagination cursor for fetching next page of results",
124
+ optional: true,
125
+ type: String)
126
+ ]
127
+ end
128
+
129
+ def output
130
+ [
131
+ ['SENTRY_ISSUES', 'Array of issue hashes with :id, :short_id, :title, :event_count, :user_count, :first_seen, :last_seen, :level, :status'],
132
+ ['SENTRY_ISSUE_COUNT', 'Number of issues returned'],
133
+ ['SENTRY_ISSUES_STATUS_CODE', 'HTTP status code from the Sentry API'],
134
+ ['SENTRY_ISSUES_JSON', 'Raw JSON response from the Sentry API']
135
+ ]
136
+ end
137
+
138
+ def return_value
139
+ "A hash with :issues (array of issue hashes) and :count (number of issues)."
140
+ end
141
+
142
+ def authors
143
+ ["crazymanish"]
144
+ end
145
+
146
+ def example_code
147
+ [
148
+ '# Fetch unresolved issues sorted by frequency
149
+ result = sentry_list_issues(query: "is:unresolved", sort: "freq", per_page: 10)
150
+ result[:issues].each do |issue|
151
+ UI.message("##{issue[:short_id]}: #{issue[:title]} (#{issue[:event_count]} events)")
152
+ end',
153
+
154
+ '# Fetch issues for a specific release
155
+ sentry_list_issues(query: "is:unresolved release:v25.10.0", sort: "freq")',
156
+
157
+ '# Fetch new issues in the latest release
158
+ sentry_list_issues(query: "is:unresolved first-release:v25.10.0", sort: "date")'
159
+ ]
160
+ end
161
+
162
+ def category
163
+ :misc
164
+ end
165
+
166
+ def is_supported?(platform)
167
+ true
168
+ end
169
+
170
+ private
171
+
172
+ def build_query_params(params)
173
+ query_params = {}
174
+
175
+ query_params[:query] = params[:query] if params[:query]
176
+ query_params[:sort] = params[:sort] if params[:sort]
177
+ query_params[:statsPeriod] = params[:stats_period] if params[:stats_period]
178
+ query_params[:per_page] = params[:per_page].to_s if params[:per_page]
179
+ query_params[:cursor] = params[:cursor] if params[:cursor]
180
+
181
+ query_params
182
+ end
183
+
184
+ def parse_response(json)
185
+ return [] unless json.is_a?(Array)
186
+
187
+ json.map do |issue|
188
+ {
189
+ id: issue['id'],
190
+ short_id: issue['shortId'],
191
+ title: issue['title'],
192
+ culprit: issue['culprit'],
193
+ level: issue['level'],
194
+ status: issue['status'],
195
+ event_count: (issue['count'] || '0').to_i,
196
+ user_count: issue['userCount'] || 0,
197
+ first_seen: issue['firstSeen'],
198
+ last_seen: issue['lastSeen'],
199
+ permalink: issue['permalink'],
200
+ metadata: issue['metadata']
201
+ }
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end