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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +333 -0
- data/lib/fastlane/plugin/sentry_api/actions/sentry_api_action.rb +129 -0
- data/lib/fastlane/plugin/sentry_api/actions/sentry_crash_free_sessions_action.rb +283 -0
- data/lib/fastlane/plugin/sentry_api/actions/sentry_crash_free_users_action.rb +198 -0
- data/lib/fastlane/plugin/sentry_api/actions/sentry_list_issues_action.rb +207 -0
- data/lib/fastlane/plugin/sentry_api/actions/sentry_slo_report_action.rb +573 -0
- data/lib/fastlane/plugin/sentry_api/actions/sentry_ttid_percentiles_action.rb +242 -0
- data/lib/fastlane/plugin/sentry_api/helper/sentry_api_helper.rb +113 -0
- data/lib/fastlane/plugin/sentry_api/version.rb +5 -0
- data/lib/fastlane/plugin/sentry_api.rb +16 -0
- metadata +51 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require_relative '../helper/sentry_api_helper'
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Actions
|
|
6
|
+
module SharedValues
|
|
7
|
+
SENTRY_TTID_DATA = :SENTRY_TTID_DATA
|
|
8
|
+
SENTRY_TTID_STATUS_CODE = :SENTRY_TTID_STATUS_CODE
|
|
9
|
+
SENTRY_TTID_JSON = :SENTRY_TTID_JSON
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Query Time to Initial Display (TTID) percentiles per screen from the Sentry Events/Discover API.
|
|
13
|
+
# Returns p50, p75, p95 metrics for each screen transaction, sorted by load count.
|
|
14
|
+
class SentryTtidPercentilesAction < Action
|
|
15
|
+
class << self
|
|
16
|
+
def run(params)
|
|
17
|
+
auth_token = params[:auth_token]
|
|
18
|
+
org_slug = params[:org_slug]
|
|
19
|
+
project_id = params[:project_id]
|
|
20
|
+
|
|
21
|
+
query_params = build_query_params(params, project_id)
|
|
22
|
+
|
|
23
|
+
UI.message("Fetching TTID percentiles from Sentry (#{query_params[:statsPeriod] || 'custom range'})...")
|
|
24
|
+
|
|
25
|
+
response = Helper::SentryApiHelper.get_events(
|
|
26
|
+
auth_token: auth_token,
|
|
27
|
+
org_slug: org_slug,
|
|
28
|
+
params: query_params
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
status_code = response[:status]
|
|
32
|
+
json = response[:json]
|
|
33
|
+
|
|
34
|
+
unless status_code.between?(200, 299)
|
|
35
|
+
UI.user_error!("Sentry Events API error #{status_code}: #{response[:body]}")
|
|
36
|
+
return nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
result = parse_response(json)
|
|
40
|
+
|
|
41
|
+
Actions.lane_context[SharedValues::SENTRY_TTID_STATUS_CODE] = status_code
|
|
42
|
+
Actions.lane_context[SharedValues::SENTRY_TTID_JSON] = json
|
|
43
|
+
Actions.lane_context[SharedValues::SENTRY_TTID_DATA] = result
|
|
44
|
+
|
|
45
|
+
UI.success("Fetched TTID data for #{result.length} screens")
|
|
46
|
+
result.first(5).each do |screen|
|
|
47
|
+
UI.message(" #{screen[:transaction]}: p50=#{screen[:p50]}ms p75=#{screen[:p75]}ms p95=#{screen[:p95]}ms (#{screen[:count]} loads)")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#####################################################
|
|
54
|
+
# @!group Documentation
|
|
55
|
+
#####################################################
|
|
56
|
+
|
|
57
|
+
def description
|
|
58
|
+
"Query TTID (Time to Initial Display) percentiles per screen from Sentry"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def details
|
|
62
|
+
[
|
|
63
|
+
"Queries the Sentry Events/Discover API for Time to Initial Display (TTID) metrics.",
|
|
64
|
+
"Returns p50, p75, and p95 percentiles per screen transaction, sorted by load count.",
|
|
65
|
+
"Useful for monitoring app launch performance and screen rendering latency.",
|
|
66
|
+
"",
|
|
67
|
+
"Supports filtering by release, environment, and transaction operation type.",
|
|
68
|
+
"Use `stats_period` for rolling windows or `start_date` + `end_date` for specific ranges.",
|
|
69
|
+
"",
|
|
70
|
+
"API Documentation: https://docs.sentry.io/api/discover/"
|
|
71
|
+
].join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def available_options
|
|
75
|
+
[
|
|
76
|
+
FastlaneCore::ConfigItem.new(key: :auth_token,
|
|
77
|
+
env_name: "SENTRY_AUTH_TOKEN",
|
|
78
|
+
description: "Sentry API Bearer auth token",
|
|
79
|
+
optional: false,
|
|
80
|
+
type: String,
|
|
81
|
+
sensitive: true,
|
|
82
|
+
code_gen_sensitive: true,
|
|
83
|
+
verify_block: proc do |value|
|
|
84
|
+
UI.user_error!("No Sentry auth token given, pass using `auth_token: 'token'`") if value.to_s.empty?
|
|
85
|
+
end),
|
|
86
|
+
FastlaneCore::ConfigItem.new(key: :org_slug,
|
|
87
|
+
env_name: "SENTRY_ORG_SLUG",
|
|
88
|
+
description: "Sentry organization slug",
|
|
89
|
+
optional: false,
|
|
90
|
+
type: String,
|
|
91
|
+
verify_block: proc do |value|
|
|
92
|
+
UI.user_error!("No Sentry org slug given, pass using `org_slug: 'my-org'`") if value.to_s.empty?
|
|
93
|
+
end),
|
|
94
|
+
FastlaneCore::ConfigItem.new(key: :project_id,
|
|
95
|
+
env_name: "SENTRY_PROJECT_ID",
|
|
96
|
+
description: "Sentry numeric project ID",
|
|
97
|
+
optional: false,
|
|
98
|
+
type: String,
|
|
99
|
+
verify_block: proc do |value|
|
|
100
|
+
UI.user_error!("No Sentry project ID given, pass using `project_id: '12345'`") if value.to_s.empty?
|
|
101
|
+
end),
|
|
102
|
+
FastlaneCore::ConfigItem.new(key: :environment,
|
|
103
|
+
env_name: "SENTRY_ENVIRONMENT",
|
|
104
|
+
description: "Environment filter (e.g. 'production')",
|
|
105
|
+
optional: true,
|
|
106
|
+
default_value: "production",
|
|
107
|
+
type: String),
|
|
108
|
+
FastlaneCore::ConfigItem.new(key: :stats_period,
|
|
109
|
+
description: "Rolling time window (e.g. '7d', '14d', '30d')",
|
|
110
|
+
optional: true,
|
|
111
|
+
default_value: "7d",
|
|
112
|
+
type: String),
|
|
113
|
+
FastlaneCore::ConfigItem.new(key: :start_date,
|
|
114
|
+
description: "Start date in ISO 8601 format. Use with end_date instead of stats_period",
|
|
115
|
+
optional: true,
|
|
116
|
+
type: String),
|
|
117
|
+
FastlaneCore::ConfigItem.new(key: :end_date,
|
|
118
|
+
description: "End date in ISO 8601 format. Use with start_date instead of stats_period",
|
|
119
|
+
optional: true,
|
|
120
|
+
type: String),
|
|
121
|
+
FastlaneCore::ConfigItem.new(key: :release,
|
|
122
|
+
description: "Filter by specific release version",
|
|
123
|
+
optional: true,
|
|
124
|
+
type: String),
|
|
125
|
+
FastlaneCore::ConfigItem.new(key: :transaction_op,
|
|
126
|
+
description: "Transaction operation filter (e.g. 'ui.load', 'ui.action')",
|
|
127
|
+
optional: true,
|
|
128
|
+
default_value: "ui.load",
|
|
129
|
+
type: String),
|
|
130
|
+
FastlaneCore::ConfigItem.new(key: :per_page,
|
|
131
|
+
description: "Number of screens to return (max 100)",
|
|
132
|
+
optional: true,
|
|
133
|
+
default_value: 20,
|
|
134
|
+
type: Integer),
|
|
135
|
+
FastlaneCore::ConfigItem.new(key: :sort,
|
|
136
|
+
description: "Sort order (e.g. '-count()', '-p95(measurements.time_to_initial_display)')",
|
|
137
|
+
optional: true,
|
|
138
|
+
default_value: "-count()",
|
|
139
|
+
type: String)
|
|
140
|
+
]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def output
|
|
144
|
+
[
|
|
145
|
+
['SENTRY_TTID_DATA', 'Array of hashes with :transaction, :p50, :p75, :p95, :count per screen'],
|
|
146
|
+
['SENTRY_TTID_STATUS_CODE', 'HTTP status code from the Sentry API'],
|
|
147
|
+
['SENTRY_TTID_JSON', 'Raw JSON response from the Sentry API']
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def return_value
|
|
152
|
+
"An array of hashes, each with :transaction (screen name), :p50, :p75, :p95 (in ms), and :count (number of loads)."
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def authors
|
|
156
|
+
["crazymanish"]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def example_code
|
|
160
|
+
[
|
|
161
|
+
'# Top 10 screens by load count, last 7 days
|
|
162
|
+
screens = sentry_ttid_percentiles(stats_period: "7d", per_page: 10)
|
|
163
|
+
screens.each do |s|
|
|
164
|
+
UI.message("#{s[:transaction]}: p50=#{s[:p50]}ms p95=#{s[:p95]}ms (#{s[:count]} loads)")
|
|
165
|
+
end',
|
|
166
|
+
|
|
167
|
+
'# Filter by release
|
|
168
|
+
sentry_ttid_percentiles(release: "v25.10.0", stats_period: "14d")',
|
|
169
|
+
|
|
170
|
+
'# Custom date range (for week-over-week comparison)
|
|
171
|
+
sentry_ttid_percentiles(start_date: "2026-02-24T00:00:00Z", end_date: "2026-03-03T00:00:00Z")'
|
|
172
|
+
]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def category
|
|
176
|
+
:misc
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def is_supported?(platform)
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def build_query_params(params, project_id)
|
|
186
|
+
fields = [
|
|
187
|
+
'transaction',
|
|
188
|
+
'p50(measurements.time_to_initial_display)',
|
|
189
|
+
'p75(measurements.time_to_initial_display)',
|
|
190
|
+
'p95(measurements.time_to_initial_display)',
|
|
191
|
+
'count()'
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
# Build the query filter
|
|
195
|
+
query_parts = ["event.type:transaction"]
|
|
196
|
+
query_parts << "transaction.op:#{params[:transaction_op]}" if params[:transaction_op]
|
|
197
|
+
query_parts << "release:#{params[:release]}" if params[:release]
|
|
198
|
+
|
|
199
|
+
query_params = {
|
|
200
|
+
dataset: 'metrics',
|
|
201
|
+
field: fields,
|
|
202
|
+
project: project_id.to_s,
|
|
203
|
+
query: query_parts.join(' '),
|
|
204
|
+
sort: params[:sort] || '-count()',
|
|
205
|
+
per_page: (params[:per_page] || 20).to_s
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if params[:start_date] && params[:end_date]
|
|
209
|
+
query_params[:start] = params[:start_date]
|
|
210
|
+
query_params[:end] = params[:end_date]
|
|
211
|
+
else
|
|
212
|
+
query_params[:statsPeriod] = params[:stats_period] || '7d'
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
query_params[:environment] = params[:environment] if params[:environment]
|
|
216
|
+
|
|
217
|
+
query_params
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def parse_response(json)
|
|
221
|
+
data = json&.dig('data') || []
|
|
222
|
+
|
|
223
|
+
data.map do |row|
|
|
224
|
+
{
|
|
225
|
+
transaction: row['transaction'],
|
|
226
|
+
p50: round_ms(row['p50(measurements.time_to_initial_display)']),
|
|
227
|
+
p75: round_ms(row['p75(measurements.time_to_initial_display)']),
|
|
228
|
+
p95: round_ms(row['p95(measurements.time_to_initial_display)']),
|
|
229
|
+
count: row['count()']
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def round_ms(value)
|
|
235
|
+
return nil if value.nil?
|
|
236
|
+
|
|
237
|
+
value.round(1)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Fastlane
|
|
7
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
|
|
8
|
+
|
|
9
|
+
module Helper
|
|
10
|
+
class SentryApiHelper
|
|
11
|
+
BASE_URL = "https://sentry.io/api/0"
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Make a GET request to the Sentry REST API.
|
|
15
|
+
#
|
|
16
|
+
# @param auth_token [String] Sentry auth token (Bearer)
|
|
17
|
+
# @param path [String] API endpoint path (e.g. "/organizations/my-org/sessions/")
|
|
18
|
+
# @param params [Hash] Query parameters. Array values produce repeated keys (field=a&field=b).
|
|
19
|
+
# @param base_url [String] Sentry API base URL
|
|
20
|
+
# @return [Hash] { status: Integer, body: String, json: Object|nil }
|
|
21
|
+
def api_request(auth_token:, path:, params: {}, base_url: BASE_URL)
|
|
22
|
+
url = "#{base_url}#{path}"
|
|
23
|
+
uri = URI(url)
|
|
24
|
+
uri.query = build_query_string(params) unless params.nil? || params.empty?
|
|
25
|
+
|
|
26
|
+
UI.verbose("Sentry API GET: #{uri}")
|
|
27
|
+
|
|
28
|
+
req = Net::HTTP::Get.new(uri)
|
|
29
|
+
req['Authorization'] = "Bearer #{auth_token}"
|
|
30
|
+
req['Content-Type'] = 'application/json'
|
|
31
|
+
|
|
32
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
|
33
|
+
if uri.scheme == 'https'
|
|
34
|
+
http.use_ssl = true
|
|
35
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
36
|
+
|
|
37
|
+
# Disable CRL checking to avoid "unable to get certificate CRL" errors
|
|
38
|
+
cert_store = OpenSSL::X509::Store.new
|
|
39
|
+
cert_store.set_default_paths
|
|
40
|
+
http.cert_store = cert_store
|
|
41
|
+
end
|
|
42
|
+
http.open_timeout = 30
|
|
43
|
+
http.read_timeout = 60
|
|
44
|
+
|
|
45
|
+
response = http.request(req)
|
|
46
|
+
|
|
47
|
+
status_code = response.code.to_i
|
|
48
|
+
body = response.body.to_s
|
|
49
|
+
json = parse_json(body)
|
|
50
|
+
|
|
51
|
+
{ status: status_code, body: body, json: json }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# GET /api/0/organizations/{org}/sessions/
|
|
55
|
+
# Used for crash-free rates, session counts, user counts.
|
|
56
|
+
def get_sessions(auth_token:, org_slug:, params: {})
|
|
57
|
+
api_request(
|
|
58
|
+
auth_token: auth_token,
|
|
59
|
+
path: "/organizations/#{org_slug}/sessions/",
|
|
60
|
+
params: params
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# GET /api/0/organizations/{org}/events/
|
|
65
|
+
# Used for Discover queries (TTID percentiles, performance metrics).
|
|
66
|
+
def get_events(auth_token:, org_slug:, params: {})
|
|
67
|
+
api_request(
|
|
68
|
+
auth_token: auth_token,
|
|
69
|
+
path: "/organizations/#{org_slug}/events/",
|
|
70
|
+
params: params
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# GET /api/0/projects/{org}/{project}/issues/
|
|
75
|
+
# Used for listing project issues filtered by release, query, etc.
|
|
76
|
+
def get_issues(auth_token:, org_slug:, project_slug:, params: {})
|
|
77
|
+
api_request(
|
|
78
|
+
auth_token: auth_token,
|
|
79
|
+
path: "/projects/#{org_slug}/#{project_slug}/issues/",
|
|
80
|
+
params: params
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Build a query string that supports repeated parameter keys via Array values.
|
|
87
|
+
# Example: { field: ['a', 'b'], project: 1 } => "field=a&field=b&project=1"
|
|
88
|
+
def build_query_string(params)
|
|
89
|
+
return nil if params.nil? || params.empty?
|
|
90
|
+
|
|
91
|
+
pairs = []
|
|
92
|
+
params.each do |key, value|
|
|
93
|
+
next if value.nil?
|
|
94
|
+
|
|
95
|
+
if value.is_a?(Array)
|
|
96
|
+
value.each { |v| pairs << [key.to_s, v.to_s] }
|
|
97
|
+
else
|
|
98
|
+
pairs << [key.to_s, value.to_s]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
URI.encode_www_form(pairs)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_json(value)
|
|
106
|
+
JSON.parse(value)
|
|
107
|
+
rescue JSON::ParserError
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'fastlane/plugin/sentry_api/version'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module SentryApi
|
|
5
|
+
# Return all .rb files inside the "actions" and "helper" directory
|
|
6
|
+
def self.all_classes
|
|
7
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# By default we want to import all available actions and helpers
|
|
13
|
+
# A plugin can contain any number of actions and plugins
|
|
14
|
+
Fastlane::SentryApi.all_classes.each do |current|
|
|
15
|
+
require current
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fastlane-plugin-sentry_api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- crazymanish
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
email: i.am.manish.rathi@gmail.com
|
|
13
|
+
executables: []
|
|
14
|
+
extensions: []
|
|
15
|
+
extra_rdoc_files: []
|
|
16
|
+
files:
|
|
17
|
+
- LICENSE
|
|
18
|
+
- README.md
|
|
19
|
+
- lib/fastlane/plugin/sentry_api.rb
|
|
20
|
+
- lib/fastlane/plugin/sentry_api/actions/sentry_api_action.rb
|
|
21
|
+
- lib/fastlane/plugin/sentry_api/actions/sentry_crash_free_sessions_action.rb
|
|
22
|
+
- lib/fastlane/plugin/sentry_api/actions/sentry_crash_free_users_action.rb
|
|
23
|
+
- lib/fastlane/plugin/sentry_api/actions/sentry_list_issues_action.rb
|
|
24
|
+
- lib/fastlane/plugin/sentry_api/actions/sentry_slo_report_action.rb
|
|
25
|
+
- lib/fastlane/plugin/sentry_api/actions/sentry_ttid_percentiles_action.rb
|
|
26
|
+
- lib/fastlane/plugin/sentry_api/helper/sentry_api_helper.rb
|
|
27
|
+
- lib/fastlane/plugin/sentry_api/version.rb
|
|
28
|
+
homepage: https://github.com/crazymanish/fastlane-plugin-sentry_api
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
rubygems_mfa_required: 'true'
|
|
33
|
+
rdoc_options: []
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.6'
|
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
requirements: []
|
|
47
|
+
rubygems_version: 3.6.7
|
|
48
|
+
specification_version: 4
|
|
49
|
+
summary: Fastlane plugin for Sentry APIs - crash-free rates, TTID percentiles, issue
|
|
50
|
+
tracking, and SLO reports
|
|
51
|
+
test_files: []
|