fastlane-plugin-sentry_api 0.1.0 → 0.2.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 +4 -4
- data/README.md +103 -4
- data/lib/fastlane/plugin/sentry_api/actions/sentry_api_action.rb +20 -20
- data/lib/fastlane/plugin/sentry_api/actions/sentry_app_launch_action.rb +260 -0
- data/lib/fastlane/plugin/sentry_api/actions/sentry_crash_free_sessions_action.rb +39 -39
- data/lib/fastlane/plugin/sentry_api/actions/sentry_crash_free_users_action.rb +31 -31
- data/lib/fastlane/plugin/sentry_api/actions/sentry_list_issues_action.rb +34 -34
- data/lib/fastlane/plugin/sentry_api/actions/sentry_slo_report_action.rb +354 -64
- data/lib/fastlane/plugin/sentry_api/actions/sentry_ttid_percentiles_action.rb +110 -43
- data/lib/fastlane/plugin/sentry_api/helper/sentry_api_helper.rb +1 -1
- data/lib/fastlane/plugin/sentry_api/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cee8dc5203226fee972dc466c125f43e872f655f4776dc703feb349a638a8ea1
|
|
4
|
+
data.tar.gz: 8d4f199b0714184b9475043557c0150da47fa83881b09de8e1ebe4b4866187e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbbb4b57bf4a3fa1530489172d9c40024e04f2b2c54ceb59ae2c79bc1f63488749528d708c7e31353212fbabf5e16c5a6ed26b893c5a2a96b9c7affed0d920e1
|
|
7
|
+
data.tar.gz: 1ef24556a924fd1a372e56e8693d8eac3c25295d4a30608cd3c72a5f064c6cc34d0ff8b96739da6cc88495858560cdc570d5fe97148ca9eb8982dde9178f2c62
|
data/README.md
CHANGED
|
@@ -21,7 +21,8 @@ A Fastlane plugin providing reusable actions for querying [Sentry](https://sentr
|
|
|
21
21
|
| [`sentry_api`](#sentry_api) | Generic GET request to any Sentry API endpoint |
|
|
22
22
|
| [`sentry_crash_free_sessions`](#sentry_crash_free_sessions) | Crash-free session & user rates from the Sessions API |
|
|
23
23
|
| [`sentry_crash_free_users`](#sentry_crash_free_users) | User-focused crash-free rate (convenience wrapper) |
|
|
24
|
-
| [`sentry_ttid_percentiles`](#sentry_ttid_percentiles) | TTID p50/p75/p95 per screen from the Discover API |
|
|
24
|
+
| [`sentry_ttid_percentiles`](#sentry_ttid_percentiles) | TTID p50/p75/p95 per screen (+ overall aggregate) from the Discover API |
|
|
25
|
+
| [`sentry_app_launch`](#sentry_app_launch) | App launch latency (cold start & warm start) percentiles |
|
|
25
26
|
| [`sentry_list_issues`](#sentry_list_issues) | Fetch project issues with filtering & sorting |
|
|
26
27
|
| [`sentry_slo_report`](#sentry_slo_report) | Comprehensive SLO report orchestrating all the above |
|
|
27
28
|
|
|
@@ -143,7 +144,7 @@ UI.message("Crash-free users: #{(rate * 100).round(2)}%")
|
|
|
143
144
|
|
|
144
145
|
### `sentry_ttid_percentiles`
|
|
145
146
|
|
|
146
|
-
Query TTID (Time to Initial Display) percentiles per screen from the Sentry Events/Discover API. Returns p50, p75, p95 per screen transaction, sorted by load count.
|
|
147
|
+
Query TTID (Time to Initial Display) percentiles per screen from the Sentry Events/Discover API. Returns p50, p75, p95 per screen transaction, sorted by load count. Optionally fetches overall/aggregate TTID percentiles across all screens.
|
|
147
148
|
|
|
148
149
|
**Parameters:**
|
|
149
150
|
|
|
@@ -160,8 +161,9 @@ Query TTID (Time to Initial Display) percentiles per screen from the Sentry Even
|
|
|
160
161
|
| `transaction_op` | `String` | No | `ui.load` | Transaction operation filter |
|
|
161
162
|
| `per_page` | `Integer` | No | `20` | Number of screens to return (max 100) |
|
|
162
163
|
| `sort` | `String` | No | `-count()` | Sort order |
|
|
164
|
+
| `include_overall` | `Boolean` | No | `false` | Also fetch overall/aggregate TTID percentiles across all screens |
|
|
163
165
|
|
|
164
|
-
**Output (SharedValues):** `SENTRY_TTID_DATA` (array of `{ transaction:, p50:, p75:, p95:, count: }`)
|
|
166
|
+
**Output (SharedValues):** `SENTRY_TTID_DATA` (array of `{ transaction:, p50:, p75:, p95:, count: }`), `SENTRY_TTID_OVERALL` (hash with `{ p50:, p75:, p95:, count: }` when `include_overall` is true)
|
|
165
167
|
|
|
166
168
|
**Examples:**
|
|
167
169
|
|
|
@@ -172,6 +174,11 @@ screens.each do |s|
|
|
|
172
174
|
UI.message("#{s[:transaction]}: p50=#{s[:p50]}ms p95=#{s[:p95]}ms (#{s[:count]} loads)")
|
|
173
175
|
end
|
|
174
176
|
|
|
177
|
+
# With overall aggregate TTID
|
|
178
|
+
screens = sentry_ttid_percentiles(stats_period: "7d", per_page: 10, include_overall: true)
|
|
179
|
+
overall = lane_context[SharedValues::SENTRY_TTID_OVERALL]
|
|
180
|
+
UI.message("Overall TTID: p50=#{overall[:p50]}ms p95=#{overall[:p95]}ms") if overall
|
|
181
|
+
|
|
175
182
|
# Filter by release
|
|
176
183
|
sentry_ttid_percentiles(release: "v25.10.0", stats_period: "14d")
|
|
177
184
|
|
|
@@ -184,6 +191,47 @@ sentry_ttid_percentiles(
|
|
|
184
191
|
|
|
185
192
|
---
|
|
186
193
|
|
|
194
|
+
### `sentry_app_launch`
|
|
195
|
+
|
|
196
|
+
Query app launch latency (cold start & warm start) percentiles from the Sentry Events/Discover API. Uses Sentry's `measurements.app_start_cold` and `measurements.app_start_warm` fields.
|
|
197
|
+
|
|
198
|
+
**Parameters:**
|
|
199
|
+
|
|
200
|
+
| Key | Type | Required | Default | Description |
|
|
201
|
+
|-----|------|----------|---------|-------------|
|
|
202
|
+
| `auth_token` | `String` | Yes | `SENTRY_AUTH_TOKEN` | API Bearer auth token |
|
|
203
|
+
| `org_slug` | `String` | Yes | `SENTRY_ORG_SLUG` | Organization slug |
|
|
204
|
+
| `project_id` | `String` | Yes | `SENTRY_PROJECT_ID` | Numeric project ID |
|
|
205
|
+
| `environment` | `String` | No | `production` | Environment filter |
|
|
206
|
+
| `stats_period` | `String` | No | `7d` | Rolling window |
|
|
207
|
+
| `start_date` | `String` | No | — | ISO 8601 start date |
|
|
208
|
+
| `end_date` | `String` | No | — | ISO 8601 end date |
|
|
209
|
+
| `release` | `String` | No | — | Filter by release version |
|
|
210
|
+
|
|
211
|
+
**Output (SharedValues):** `SENTRY_APP_LAUNCH_DATA` (hash with `:cold_start` and `:warm_start`, each containing `{ p50:, p75:, p95:, count: }`)
|
|
212
|
+
|
|
213
|
+
**Examples:**
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# Fetch app launch metrics for the last 7 days
|
|
217
|
+
result = sentry_app_launch(stats_period: "7d")
|
|
218
|
+
cold = result[:cold_start]
|
|
219
|
+
warm = result[:warm_start]
|
|
220
|
+
UI.message("Cold start: p50=#{cold[:p50]}ms p95=#{cold[:p95]}ms (#{cold[:count]} launches)")
|
|
221
|
+
UI.message("Warm start: p50=#{warm[:p50]}ms p95=#{warm[:p95]}ms (#{warm[:count]} launches)")
|
|
222
|
+
|
|
223
|
+
# Filter by release
|
|
224
|
+
sentry_app_launch(release: "v25.10.0", stats_period: "14d")
|
|
225
|
+
|
|
226
|
+
# Custom date range
|
|
227
|
+
sentry_app_launch(
|
|
228
|
+
start_date: "2026-02-24T00:00:00Z",
|
|
229
|
+
end_date: "2026-03-03T00:00:00Z"
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
187
235
|
### `sentry_list_issues`
|
|
188
236
|
|
|
189
237
|
Fetch issues from a Sentry project. Supports filtering by release version, query string, sort order, and pagination. Useful for comparing issues across releases.
|
|
@@ -226,8 +274,10 @@ sentry_list_issues(query: "is:unresolved first-release:v25.10.0", sort: "date")
|
|
|
226
274
|
Generate a comprehensive SLO report by orchestrating multiple Sentry API calls. Produces a structured report with:
|
|
227
275
|
|
|
228
276
|
- **Availability** — Crash-free session/user rates with week-over-week delta
|
|
229
|
-
- **Latency** — TTID p50/p75/p95
|
|
277
|
+
- **Latency (TTID)** — Overall/aggregate TTID percentiles + per-screen p50/p75/p95 with week-over-week delta
|
|
278
|
+
- **Latency (App Launch)** — Cold start & warm start percentiles with week-over-week delta
|
|
230
279
|
- **Release comparison** — Release-over-release crash-free rates
|
|
280
|
+
- **Top Crash Issues** — Top unhandled error issues by frequency (`is:unresolved issue.category:error error.unhandled:true`)
|
|
231
281
|
- **Issues** — Issue counts and top issues per release (latest vs previous)
|
|
232
282
|
|
|
233
283
|
Includes target checking with ✅/⚠️ indicators and optional JSON file output.
|
|
@@ -244,6 +294,7 @@ Includes target checking with ✅/⚠️ indicators and optional JSON file outpu
|
|
|
244
294
|
| `stats_period` | `String` | No | `7d` | Rolling window |
|
|
245
295
|
| `crash_free_target` | `Float` | No | `0.998` | Target crash-free session rate (e.g. 0.998 = 99.8%) |
|
|
246
296
|
| `ttid_p95_target_ms` | `Integer` | No | `1000` | Target TTID p95 in milliseconds |
|
|
297
|
+
| `app_launch_p95_target_ms` | `Integer` | No | `2000` | Target app launch (cold start) p95 in milliseconds |
|
|
247
298
|
| `compare_weeks` | `Boolean` | No | `true` | Include week-over-week comparison |
|
|
248
299
|
| `compare_releases` | `Boolean` | No | `true` | Include release-over-release comparison |
|
|
249
300
|
| `current_release` | `String` | No | — | Current release version for issue comparison |
|
|
@@ -251,10 +302,22 @@ Includes target checking with ✅/⚠️ indicators and optional JSON file outpu
|
|
|
251
302
|
| `release_count` | `Integer` | No | `5` | Number of releases to compare |
|
|
252
303
|
| `ttid_screen_count` | `Integer` | No | `10` | Number of top screens in TTID report |
|
|
253
304
|
| `issue_count` | `Integer` | No | `10` | Number of top issues per release |
|
|
305
|
+
| `crash_issue_count` | `Integer` | No | `5` | Number of top crash (unhandled error) issues to include |
|
|
254
306
|
| `output_json` | `String` | No | — | Path to write JSON report file |
|
|
255
307
|
|
|
256
308
|
**Output (SharedValues):** `SENTRY_SLO_REPORT` (complete hash with `:availability`, `:latency`, `:issues`)
|
|
257
309
|
|
|
310
|
+
The `:latency` section includes:
|
|
311
|
+
- `:current` — array of per-screen TTID data
|
|
312
|
+
- `:overall` — aggregate TTID `{ p50:, p75:, p95:, count: }` across all screens
|
|
313
|
+
- `:app_launch` — `{ cold: { p50:, p75:, p95:, count: }, warm: { ... } }`
|
|
314
|
+
- `:previous`, `:overall_previous`, `:app_launch_previous` — week-over-week counterparts (when `compare_weeks` is true)
|
|
315
|
+
|
|
316
|
+
The `:issues` section includes:
|
|
317
|
+
- `:top_crashes` — array of top unhandled error issues (always fetched, not release-scoped)
|
|
318
|
+
- `:current_release` — `{ version:, count:, issues: [] }` (when `current_release` is provided)
|
|
319
|
+
- `:previous_release` — same structure (when `previous_release` is provided)
|
|
320
|
+
|
|
258
321
|
**Examples:**
|
|
259
322
|
|
|
260
323
|
```ruby
|
|
@@ -262,6 +325,7 @@ Includes target checking with ✅/⚠️ indicators and optional JSON file outpu
|
|
|
262
325
|
sentry_slo_report(
|
|
263
326
|
crash_free_target: 0.998,
|
|
264
327
|
ttid_p95_target_ms: 1000,
|
|
328
|
+
app_launch_p95_target_ms: 2000,
|
|
265
329
|
compare_weeks: true,
|
|
266
330
|
compare_releases: true,
|
|
267
331
|
current_release: "v25.10.0",
|
|
@@ -269,6 +333,22 @@ sentry_slo_report(
|
|
|
269
333
|
output_json: "slo_report.json"
|
|
270
334
|
)
|
|
271
335
|
|
|
336
|
+
# Access report data
|
|
337
|
+
report = lane_context[SharedValues::SENTRY_SLO_REPORT]
|
|
338
|
+
|
|
339
|
+
# Overall TTID
|
|
340
|
+
overall = report[:latency][:overall]
|
|
341
|
+
UI.message("Overall TTID p95: #{overall[:p95]}ms")
|
|
342
|
+
|
|
343
|
+
# App launch
|
|
344
|
+
cold = report[:latency][:app_launch][:cold]
|
|
345
|
+
UI.message("Cold start p95: #{cold[:p95]}ms")
|
|
346
|
+
|
|
347
|
+
# Top crash issues
|
|
348
|
+
report[:issues][:top_crashes].each do |issue|
|
|
349
|
+
UI.message("#{issue[:short_id]}: #{issue[:title]} (#{issue[:event_count]} events)")
|
|
350
|
+
end
|
|
351
|
+
|
|
272
352
|
# Quick availability check only
|
|
273
353
|
report = sentry_slo_report(
|
|
274
354
|
compare_weeks: true,
|
|
@@ -292,10 +372,29 @@ lane :availability_check do
|
|
|
292
372
|
UI.important("Crash-free session rate: #{(rate * 100).round(2)}%")
|
|
293
373
|
end
|
|
294
374
|
|
|
375
|
+
lane :ttid_check do
|
|
376
|
+
screens = sentry_ttid_percentiles(stats_period: "7d", per_page: 10, include_overall: true)
|
|
377
|
+
screens.each do |s|
|
|
378
|
+
UI.message("#{s[:transaction]}: p50=#{s[:p50]}ms p95=#{s[:p95]}ms (#{s[:count]} loads)")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
overall = lane_context[SharedValues::SENTRY_TTID_OVERALL]
|
|
382
|
+
UI.important("Overall TTID: p50=#{overall[:p50]}ms p95=#{overall[:p95]}ms") if overall
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
lane :app_launch_check do
|
|
386
|
+
result = sentry_app_launch(stats_period: "7d")
|
|
387
|
+
cold = result[:cold_start]
|
|
388
|
+
warm = result[:warm_start]
|
|
389
|
+
UI.message("Cold start: p50=#{cold[:p50]}ms p95=#{cold[:p95]}ms")
|
|
390
|
+
UI.message("Warm start: p50=#{warm[:p50]}ms p95=#{warm[:p95]}ms")
|
|
391
|
+
end
|
|
392
|
+
|
|
295
393
|
lane :slo_report do
|
|
296
394
|
sentry_slo_report(
|
|
297
395
|
crash_free_target: 0.998,
|
|
298
396
|
ttid_p95_target_ms: 1000,
|
|
397
|
+
app_launch_p95_target_ms: 2000,
|
|
299
398
|
current_release: "v25.10.0",
|
|
300
399
|
previous_release: "v25.9.0",
|
|
301
400
|
output_json: "slo_report.json"
|
|
@@ -59,32 +59,32 @@ module Fastlane
|
|
|
59
59
|
def available_options
|
|
60
60
|
[
|
|
61
61
|
FastlaneCore::ConfigItem.new(key: :auth_token,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
env_name: "SENTRY_AUTH_TOKEN",
|
|
63
|
+
description: "Sentry API Bearer auth token",
|
|
64
|
+
optional: false,
|
|
65
65
|
type: String,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
sensitive: true,
|
|
67
|
+
code_gen_sensitive: true,
|
|
68
|
+
verify_block: proc do |value|
|
|
69
|
+
UI.user_error!("No Sentry auth token given, pass using `auth_token: 'token'`") if value.to_s.empty?
|
|
70
|
+
end),
|
|
71
71
|
FastlaneCore::ConfigItem.new(key: :server_url,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
env_name: "SENTRY_API_SERVER_URL",
|
|
73
|
+
description: "Sentry API base URL",
|
|
74
|
+
optional: true,
|
|
75
|
+
default_value: "https://sentry.io/api/0",
|
|
76
76
|
type: String),
|
|
77
77
|
FastlaneCore::ConfigItem.new(key: :path,
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
description: "API endpoint path (e.g. '/organizations/my-org/sessions/')",
|
|
79
|
+
optional: false,
|
|
80
80
|
type: String,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
verify_block: proc do |value|
|
|
82
|
+
UI.user_error!("API path cannot be empty") if value.to_s.empty?
|
|
83
|
+
end),
|
|
84
84
|
FastlaneCore::ConfigItem.new(key: :params,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
description: "Query parameters hash. Array values produce repeated keys.",
|
|
86
|
+
optional: true,
|
|
87
|
+
default_value: {},
|
|
88
88
|
type: Hash)
|
|
89
89
|
]
|
|
90
90
|
end
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require_relative '../helper/sentry_api_helper'
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Actions
|
|
6
|
+
module SharedValues
|
|
7
|
+
SENTRY_APP_LAUNCH_DATA = :SENTRY_APP_LAUNCH_DATA
|
|
8
|
+
SENTRY_APP_LAUNCH_STATUS_CODE = :SENTRY_APP_LAUNCH_STATUS_CODE
|
|
9
|
+
SENTRY_APP_LAUNCH_JSON = :SENTRY_APP_LAUNCH_JSON
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Query app launch (cold start & warm start) percentiles from the Sentry Events/Discover API.
|
|
13
|
+
# Returns p50, p75, p95 metrics for cold start and warm start durations.
|
|
14
|
+
class SentryAppLaunchAction < 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
|
+
result = {}
|
|
22
|
+
|
|
23
|
+
# Fetch cold start metrics
|
|
24
|
+
cold_params = build_query_params(params, project_id, :cold)
|
|
25
|
+
UI.message("Fetching cold start metrics from Sentry (#{cold_params[:statsPeriod] || 'custom range'})...")
|
|
26
|
+
|
|
27
|
+
cold_response = Helper::SentryApiHelper.get_events(
|
|
28
|
+
auth_token: auth_token,
|
|
29
|
+
org_slug: org_slug,
|
|
30
|
+
params: cold_params
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
unless cold_response[:status].between?(200, 299)
|
|
34
|
+
UI.user_error!("Sentry Events API error #{cold_response[:status]}: #{cold_response[:body]}")
|
|
35
|
+
return nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result[:cold_start] = parse_response(cold_response[:json], :cold)
|
|
39
|
+
|
|
40
|
+
# Fetch warm start metrics
|
|
41
|
+
warm_params = build_query_params(params, project_id, :warm)
|
|
42
|
+
UI.message("Fetching warm start metrics from Sentry...")
|
|
43
|
+
|
|
44
|
+
warm_response = Helper::SentryApiHelper.get_events(
|
|
45
|
+
auth_token: auth_token,
|
|
46
|
+
org_slug: org_slug,
|
|
47
|
+
params: warm_params
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
unless warm_response[:status].between?(200, 299)
|
|
51
|
+
UI.user_error!("Sentry Events API error #{warm_response[:status]}: #{warm_response[:body]}")
|
|
52
|
+
return nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result[:warm_start] = parse_response(warm_response[:json], :warm)
|
|
56
|
+
|
|
57
|
+
Actions.lane_context[SharedValues::SENTRY_APP_LAUNCH_STATUS_CODE] = cold_response[:status]
|
|
58
|
+
Actions.lane_context[SharedValues::SENTRY_APP_LAUNCH_JSON] = {
|
|
59
|
+
cold: cold_response[:json],
|
|
60
|
+
warm: warm_response[:json]
|
|
61
|
+
}
|
|
62
|
+
Actions.lane_context[SharedValues::SENTRY_APP_LAUNCH_DATA] = result
|
|
63
|
+
|
|
64
|
+
log_result(result)
|
|
65
|
+
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#####################################################
|
|
70
|
+
# @!group Documentation
|
|
71
|
+
#####################################################
|
|
72
|
+
|
|
73
|
+
def description
|
|
74
|
+
"Query app launch (cold start & warm start) latency percentiles from Sentry"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def details
|
|
78
|
+
[
|
|
79
|
+
"Queries the Sentry Events/Discover API for app launch latency metrics.",
|
|
80
|
+
"Returns p50, p75, and p95 percentiles for both cold start and warm start durations.",
|
|
81
|
+
"",
|
|
82
|
+
"Cold start: Full app initialization from a terminated state.",
|
|
83
|
+
"Warm start: App resume from a backgrounded/cached state.",
|
|
84
|
+
"",
|
|
85
|
+
"Uses Sentry's `measurements.app_start_cold` and `measurements.app_start_warm` fields.",
|
|
86
|
+
"Supports filtering by release, environment, and time range.",
|
|
87
|
+
"",
|
|
88
|
+
"API Documentation: https://docs.sentry.io/api/discover/"
|
|
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')",
|
|
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. 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. Use with start_date instead of stats_period",
|
|
137
|
+
optional: true,
|
|
138
|
+
type: String),
|
|
139
|
+
FastlaneCore::ConfigItem.new(key: :release,
|
|
140
|
+
description: "Filter by specific release version",
|
|
141
|
+
optional: true,
|
|
142
|
+
type: String)
|
|
143
|
+
]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def output
|
|
147
|
+
[
|
|
148
|
+
['SENTRY_APP_LAUNCH_DATA', 'Hash with :cold_start and :warm_start, each containing :p50, :p75, :p95, :count'],
|
|
149
|
+
['SENTRY_APP_LAUNCH_STATUS_CODE', 'HTTP status code from the Sentry API'],
|
|
150
|
+
['SENTRY_APP_LAUNCH_JSON', 'Raw JSON responses from the Sentry API (cold and warm)']
|
|
151
|
+
]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def return_value
|
|
155
|
+
"A hash with :cold_start and :warm_start keys, each containing :p50, :p75, :p95 (in ms), and :count."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def authors
|
|
159
|
+
["crazymanish"]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def example_code
|
|
163
|
+
[
|
|
164
|
+
'# Fetch app launch metrics for the last 7 days
|
|
165
|
+
result = sentry_app_launch(stats_period: "7d")
|
|
166
|
+
cold = result[:cold_start]
|
|
167
|
+
warm = result[:warm_start]
|
|
168
|
+
UI.message("Cold start: p50=#{cold[:p50]}ms p95=#{cold[:p95]}ms")
|
|
169
|
+
UI.message("Warm start: p50=#{warm[:p50]}ms p95=#{warm[:p95]}ms")',
|
|
170
|
+
|
|
171
|
+
'# Filter by release
|
|
172
|
+
sentry_app_launch(release: "v25.10.0", stats_period: "14d")',
|
|
173
|
+
|
|
174
|
+
'# Custom date range
|
|
175
|
+
sentry_app_launch(start_date: "2026-02-24T00:00:00Z", end_date: "2026-03-03T00:00:00Z")'
|
|
176
|
+
]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def category
|
|
180
|
+
:misc
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def is_supported?(platform)
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def build_query_params(params, project_id, start_type)
|
|
190
|
+
measurement = start_type == :cold ? 'app_start_cold' : 'app_start_warm'
|
|
191
|
+
|
|
192
|
+
fields = [
|
|
193
|
+
"p50(measurements.#{measurement})",
|
|
194
|
+
"p75(measurements.#{measurement})",
|
|
195
|
+
"p95(measurements.#{measurement})",
|
|
196
|
+
'count()'
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
query_parts = ['event.type:transaction']
|
|
200
|
+
query_parts << "has:measurements.#{measurement}"
|
|
201
|
+
query_parts << "release:#{params[:release]}" if params[:release]
|
|
202
|
+
|
|
203
|
+
query_params = {
|
|
204
|
+
dataset: 'metrics',
|
|
205
|
+
field: fields,
|
|
206
|
+
project: project_id.to_s,
|
|
207
|
+
query: query_parts.join(' '),
|
|
208
|
+
per_page: '1'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if params[:start_date] && params[:end_date]
|
|
212
|
+
query_params[:start] = params[:start_date]
|
|
213
|
+
query_params[:end] = params[:end_date]
|
|
214
|
+
else
|
|
215
|
+
query_params[:statsPeriod] = params[:stats_period] || '7d'
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
query_params[:environment] = params[:environment] if params[:environment]
|
|
219
|
+
|
|
220
|
+
query_params
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def parse_response(json, start_type)
|
|
224
|
+
measurement = start_type == :cold ? 'app_start_cold' : 'app_start_warm'
|
|
225
|
+
row = json&.dig('data', 0) || {}
|
|
226
|
+
|
|
227
|
+
{
|
|
228
|
+
p50: round_ms(row["p50(measurements.#{measurement})"]),
|
|
229
|
+
p75: round_ms(row["p75(measurements.#{measurement})"]),
|
|
230
|
+
p95: round_ms(row["p95(measurements.#{measurement})"]),
|
|
231
|
+
count: row['count()']
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def round_ms(value)
|
|
236
|
+
return nil if value.nil?
|
|
237
|
+
|
|
238
|
+
value.round(1)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def log_result(result)
|
|
242
|
+
cold = result[:cold_start]
|
|
243
|
+
warm = result[:warm_start]
|
|
244
|
+
|
|
245
|
+
if cold[:p50]
|
|
246
|
+
UI.success("Cold start: p50=#{cold[:p50]}ms p75=#{cold[:p75]}ms p95=#{cold[:p95]}ms (#{cold[:count]} launches)")
|
|
247
|
+
else
|
|
248
|
+
UI.message("Cold start: no data available")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if warm[:p50]
|
|
252
|
+
UI.success("Warm start: p50=#{warm[:p50]}ms p75=#{warm[:p75]}ms p95=#{warm[:p95]}ms (#{warm[:count]} launches)")
|
|
253
|
+
else
|
|
254
|
+
UI.message("Warm start: no data available")
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -92,63 +92,63 @@ module Fastlane
|
|
|
92
92
|
def available_options
|
|
93
93
|
[
|
|
94
94
|
FastlaneCore::ConfigItem.new(key: :auth_token,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
env_name: "SENTRY_AUTH_TOKEN",
|
|
96
|
+
description: "Sentry API Bearer auth token",
|
|
97
|
+
optional: false,
|
|
98
98
|
type: String,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
104
|
FastlaneCore::ConfigItem.new(key: :org_slug,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
env_name: "SENTRY_ORG_SLUG",
|
|
106
|
+
description: "Sentry organization slug",
|
|
107
|
+
optional: false,
|
|
108
108
|
type: String,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
112
|
FastlaneCore::ConfigItem.new(key: :project_id,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
env_name: "SENTRY_PROJECT_ID",
|
|
114
|
+
description: "Sentry numeric project ID",
|
|
115
|
+
optional: false,
|
|
116
116
|
type: String,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
120
|
FastlaneCore::ConfigItem.new(key: :environment,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
env_name: "SENTRY_ENVIRONMENT",
|
|
122
|
+
description: "Environment filter (e.g. 'production')",
|
|
123
|
+
optional: true,
|
|
124
|
+
default_value: "production",
|
|
125
125
|
type: String),
|
|
126
126
|
FastlaneCore::ConfigItem.new(key: :stats_period,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
130
|
type: String),
|
|
131
131
|
FastlaneCore::ConfigItem.new(key: :start_date,
|
|
132
|
-
|
|
133
|
-
|
|
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
134
|
type: String),
|
|
135
135
|
FastlaneCore::ConfigItem.new(key: :end_date,
|
|
136
|
-
|
|
137
|
-
|
|
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
138
|
type: String),
|
|
139
139
|
FastlaneCore::ConfigItem.new(key: :group_by,
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
description: "Group results by dimension: 'release', 'environment', or nil for aggregate",
|
|
141
|
+
optional: true,
|
|
142
142
|
type: String),
|
|
143
143
|
FastlaneCore::ConfigItem.new(key: :per_page,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
description: "Number of groups to return when group_by is set (max 100)",
|
|
145
|
+
optional: true,
|
|
146
|
+
default_value: 10,
|
|
147
147
|
type: Integer),
|
|
148
148
|
FastlaneCore::ConfigItem.new(key: :order_by,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
description: "Sort order for grouped results (e.g. '-sum(session)', '-crash_free_rate(session)')",
|
|
150
|
+
optional: true,
|
|
151
|
+
default_value: "-sum(session)",
|
|
152
152
|
type: String)
|
|
153
153
|
]
|
|
154
154
|
end
|