luciq-cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,585 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'luciq/commands/query'
5
+
6
+ module Luciq
7
+ # Shared Thor option groups for query commands.
8
+ module QueryOptions
9
+ MODES = %w[beta production staging alpha qa development].freeze
10
+
11
+ def self.app(thor)
12
+ thor.option :slug, type: :string, required: true, desc: 'Application slug'
13
+ thor.option :mode, type: :string, required: true, enum: MODES, desc: 'Application mode'
14
+ end
15
+
16
+ def self.page(thor)
17
+ thor.option :offset, type: :numeric, desc: 'Number of results to skip'
18
+ thor.option :limit, type: :numeric, desc: 'Maximum number of results to return'
19
+ end
20
+
21
+ def self.sort(thor, sort_by_enum: nil)
22
+ thor.option :sort_by, type: :string, enum: sort_by_enum, desc: 'Sort field'
23
+ thor.option :direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
24
+ end
25
+
26
+ def self.raw_filters(thor)
27
+ thor.option :filters, type: :string,
28
+ desc: 'Raw filter JSON object, merged in (typed filter flags win on key conflicts)'
29
+ end
30
+ end
31
+
32
+ class CrashesCLI < Thor
33
+ SORT_FIELDS = %w[last_occurred_at occurrences_counter affected_users_counter
34
+ max_app_version min_app_version severity first_occurred_at].freeze
35
+
36
+ desc 'list', 'List crashes for an application'
37
+ long_desc(<<~DESC, wrap: false)
38
+ List crashes for an application.
39
+
40
+ Common filters have dedicated flags: --status, --platform, --type, --app-version.
41
+ Pass any other supported filter as a JSON object via --filters:
42
+ date_ms {"gte": <ms>, "lte": <ms>} occurrence time range
43
+ status_id [1,2,3] (1=open 2=closed 3=in_progress)
44
+ teams ["<team-id>", ...]
45
+ app_versions ["1.2.3", ...]
46
+ devices ["iPhone15,2", ...]
47
+ os_versions ["17.4", ...]
48
+ platform ["IOS","ANDROID","DART","JAVASCRIPT"]
49
+ current_views ["<screen-name>", ...]
50
+ type ["CRASH","ANR","OOM","NON_FATAL"]
51
+ subtype ["CRITICAL","ERROR","WARNING","INFO"] (needs NON_FATAL in type)
52
+ feature_flags ["<flag>" or "<flag> -> <variant>", ...]
53
+
54
+ Example: --filters '{"devices":["iPhone15,2"],"type":["NON_FATAL"]}'
55
+ DESC
56
+ QueryOptions.app(self)
57
+ QueryOptions.page(self)
58
+ QueryOptions.sort(self, sort_by_enum: SORT_FIELDS)
59
+ option :status, type: :array, enum: %w[open closed in_progress], desc: 'Filter by status'
60
+ option :platform, type: :array, enum: %w[IOS ANDROID DART JAVASCRIPT], desc: 'Filter by platform'
61
+ option :type, type: :array, enum: %w[CRASH ANR OOM NON_FATAL], desc: 'Filter by crash type'
62
+ option :app_version, type: :array, desc: 'Filter by app version(s)'
63
+ QueryOptions.raw_filters(self)
64
+ def list
65
+ Commands::Query.new(options).crashes_list
66
+ end
67
+
68
+ desc 'show', 'Show details for a single crash'
69
+ QueryOptions.app(self)
70
+ option :number, type: :numeric, required: true, desc: 'Crash number'
71
+ def show
72
+ Commands::Query.new(options).crash_show(options[:number])
73
+ end
74
+
75
+ desc 'patterns', 'Show crash patterns for a single crash'
76
+ long_desc(<<~DESC, wrap: false)
77
+ Show crash patterns (grouped dimensions) for a single crash.
78
+
79
+ Typed flags: --pattern-key, --sort-by, --direction. Other filters via --filters:
80
+ date_ms {"gte": <ms>, "lte": <ms>}
81
+ app_versions ["1.2.3", ...]
82
+ devices ["iPhone15,2", ...]
83
+ os_versions ["17.4", ...]
84
+ DESC
85
+ QueryOptions.app(self)
86
+ option :number, type: :numeric, required: true, desc: 'Crash number'
87
+ option :pattern_key, type: :string,
88
+ enum: %w[app_versions devices oses current_views app_status experiments],
89
+ desc: 'Pattern dimension'
90
+ option :sort_by, type: :string, enum: %w[occurrences_count last_seen first_seen], desc: 'Sort field'
91
+ option :direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
92
+ QueryOptions.raw_filters(self)
93
+ def patterns
94
+ Commands::Query.new(options).crash_patterns(options[:number])
95
+ end
96
+
97
+ desc 'diagnostics', 'Show diagnostics for a single crash'
98
+ QueryOptions.app(self)
99
+ option :number, type: :numeric, required: true, desc: 'Crash number'
100
+ def diagnostics
101
+ Commands::Query.new(options).crash_diagnostics(options[:number])
102
+ end
103
+
104
+ desc 'hangs', 'List app hangs for an application'
105
+ long_desc(<<~DESC, wrap: false)
106
+ List app hangs for an application.
107
+
108
+ Typed flags: --status, --platform, --app-version. Other filters via --filters:
109
+ date_ms {"gte": <ms>, "lte": <ms>}
110
+ status_id [1,2,3]
111
+ teams ["<team-id>", ...]
112
+ app_versions ["1.2.3", ...]
113
+ devices ["iPhone15,2", ...]
114
+ os_versions ["17.4", ...]
115
+ platform ["IOS","ANDROID","DART","JAVASCRIPT"]
116
+ current_views ["<screen-name>", ...]
117
+ DESC
118
+ QueryOptions.app(self)
119
+ QueryOptions.page(self)
120
+ QueryOptions.sort(self, sort_by_enum: SORT_FIELDS)
121
+ option :status, type: :array, enum: %w[open closed in_progress], desc: 'Filter by status'
122
+ option :platform, type: :array, enum: %w[IOS ANDROID DART JAVASCRIPT], desc: 'Filter by platform'
123
+ option :app_version, type: :array, desc: 'Filter by app version(s)'
124
+ QueryOptions.raw_filters(self)
125
+ def hangs
126
+ Commands::Query.new(options).app_hangs
127
+ end
128
+
129
+ desc 'occurrence-tokens', 'List occurrence tokens for a crash'
130
+ long_desc(<<~DESC, wrap: false)
131
+ List occurrence tokens for a crash (paginate with --current-token / --direction).
132
+
133
+ Filters via --filters:
134
+ date_ms {"gte": <ms>, "lte": <ms>}
135
+ app_versions ["1.2.3", ...]
136
+ app_status "foreground" | "background"
137
+ devices ["iPhone15,2", ...]
138
+ os_versions ["17.4", ...]
139
+ experiments ["<experiment>", ...]
140
+ current_views ["<screen-name>", ...]
141
+ DESC
142
+ QueryOptions.app(self)
143
+ option :number, type: :numeric, required: true, desc: 'Crash number'
144
+ option :current_token, type: :string, desc: 'Pagination cursor token'
145
+ option :direction, type: :string, enum: %w[first last], desc: 'Pagination direction'
146
+ QueryOptions.raw_filters(self)
147
+ def occurrence_tokens
148
+ Commands::Query.new(options).occurrence_tokens(options[:number])
149
+ end
150
+
151
+ desc 'occurrence', 'Show details for a single crash occurrence'
152
+ QueryOptions.app(self)
153
+ option :number, type: :numeric, required: true, desc: 'Crash number'
154
+ option :ulid, type: :string, required: true, desc: 'State/occurrence ULID token'
155
+ def occurrence
156
+ Commands::Query.new(options).occurrence_details(options[:number], options[:ulid])
157
+ end
158
+ end
159
+
160
+ class BugsCLI < Thor
161
+ desc 'list', 'List bugs for an application'
162
+ long_desc(<<~DESC, wrap: false)
163
+ List bugs for an application.
164
+
165
+ Common filters have dedicated flags: --status, --priority, --app-version.
166
+ Pass any other supported filter as a JSON object via --filters:
167
+ status_id [1,2,3] (1=New 2=Closed 3=In Progress)
168
+ priority_id [-1,1,2,3,4] (-1=N/A 1=Trivial 2=Minor 3=Major 4=Blocker)
169
+ app_version ["1.2.3", ...]
170
+ platform ["ios","android"] (cross-platform apps)
171
+ type ["<type>", ...]
172
+ tag [["login","auth"], ...] (inner array OR-ed, groups AND-ed)
173
+ category [["UI"], ...]
174
+ devices ["iPhone 15 Pro", ...]
175
+ os_versions ["iOS 17.0", ...]
176
+ experiments ["<flag>", ...]
177
+ reported_at {"from": <ms>, "to": <ms>}
178
+ DESC
179
+ QueryOptions.app(self)
180
+ QueryOptions.page(self)
181
+ QueryOptions.sort(self)
182
+ option :status, type: :array, enum: %w[new closed in_progress], desc: 'Filter by status'
183
+ option :priority, type: :array, enum: %w[na trivial minor major blocker], desc: 'Filter by priority'
184
+ option :app_version, type: :array, desc: 'Filter by app version(s)'
185
+ QueryOptions.raw_filters(self)
186
+ def list
187
+ Commands::Query.new(options).bugs_list
188
+ end
189
+
190
+ desc 'show', 'Show details for a single bug'
191
+ QueryOptions.app(self)
192
+ option :number, type: :numeric, required: true, desc: 'Bug number'
193
+ def show
194
+ Commands::Query.new(options).bug_show(options[:number])
195
+ end
196
+
197
+ desc 'update', 'Update a bug (status, priority, tags, or mark duplicate)'
198
+ long_desc(<<~DESC, wrap: false)
199
+ Update a bug. Provide at least one change: --status/--priority/--tags/--clear-tags
200
+ change the bug; duplicate marking is a separate action and can't be combined with them.
201
+
202
+ Tags: --tags appends to the existing tags by default. Use --tag-action to change
203
+ that: replace sets --tags as the full list (removing others); remove deletes --tags
204
+ and keeps the rest. --clear-tags removes all tags.
205
+
206
+ Mark a duplicate: --duplicate-of <master-number> (implies --action mark_as_duplicate).
207
+ Detach a duplicate: --action unmark_as_duplicate.
208
+ DESC
209
+ QueryOptions.app(self)
210
+ option :number, type: :numeric, required: true, desc: 'Bug number'
211
+ option :status, type: :string, enum: %w[new closed in_progress], desc: 'New status'
212
+ option :priority, type: :string, enum: %w[na trivial minor major blocker], desc: 'New priority'
213
+ option :tags, type: :array, desc: 'Tags to apply (see --tag-action; appends to existing tags by default)'
214
+ option :tag_action, type: :string, enum: %w[replace append remove],
215
+ desc: 'How --tags is applied: append (default, keep existing), replace, remove'
216
+ option :clear_tags, type: :boolean, desc: 'Remove all tags (cannot be combined with --tag-action)'
217
+ option :duplicate_of, type: :numeric, desc: 'Master bug number (marks this a duplicate)'
218
+ option :action, type: :string, enum: %w[mark_as_duplicate unmark_as_duplicate],
219
+ desc: 'Duplicate action (defaults to mark_as_duplicate with --duplicate-of)'
220
+ def update
221
+ Commands::Query.new(options).bug_update(options[:number])
222
+ end
223
+ end
224
+
225
+ class ApmCLI < Thor
226
+ METRICS = %w[network launch flows screen_loading frame_drop].freeze
227
+ GROUP_METRICS = (METRICS + %w[funnels]).freeze
228
+
229
+ desc 'groups', 'Rank APM groups worst-first for an application'
230
+ long_desc(<<~DESC, wrap: false)
231
+ Rank APM groups worst-first. --metric is required (network, launch, flows,
232
+ screen_loading, frame_drop, funnels).
233
+
234
+ --sort is a JSON object {"by": <field>, "direction": "asc"|"desc"}; the CLI
235
+ wraps it in the single-item array the API expects.
236
+ by: failure_rate, p95, p50, apdex, apdex_change, occurrences, dissat_count,
237
+ frozen_frames_percent, slow_frames_percent, count, conversion, drop_off, median_time
238
+
239
+ --filters is a JSON object. Common keys: date_ms {"gte","lte"},
240
+ app_version ["1.2.3"], platform ["ios","android"], group_name, key_metric,
241
+ count, dissat_count, apdex, apdex_change, teams. Available filters vary by
242
+ --metric; see the Luciq API docs for the full per-metric set.
243
+ DESC
244
+ QueryOptions.app(self)
245
+ option :metric, type: :string, required: true, enum: GROUP_METRICS, desc: 'APM metric'
246
+ QueryOptions.page(self)
247
+ option :sort, type: :string, desc: 'Sort JSON object {"by":...,"direction":...} (wrapped in a 1-item array)'
248
+ QueryOptions.raw_filters(self)
249
+ def groups
250
+ Commands::Query.new(options).apm_groups
251
+ end
252
+
253
+ desc 'group', 'Fetch APM panels for a single group'
254
+ long_desc(<<~DESC, wrap: false)
255
+ Fetch APM panels (charts/tables/summary) for a single group. --metric is
256
+ required; identify the group with --group-uuid or --group-url. --method
257
+ applies to the network metric.
258
+
259
+ --views is a JSON array selecting which panels/tables to return (defaults to ["summary"]).
260
+ --filters is a JSON object; keys vary by metric and include date_ms,
261
+ app_version, platform ["ios","android"], device, os_version, country,
262
+ carrier, radio, failure_name, failure_type, response_time_ms,
263
+ custom_attributes, experiment. See the Luciq API docs for the full schema.
264
+ DESC
265
+ QueryOptions.app(self)
266
+ option :metric, type: :string, required: true, enum: GROUP_METRICS, desc: 'APM metric'
267
+ option :group_uuid, type: :string, desc: 'Group UUID (or use --group-url)'
268
+ option :group_url, type: :string, desc: 'Group URL (or use --group-uuid)'
269
+ option :method, type: :string, enum: %w[GET POST PUT PATCH DELETE HEAD OPTIONS], desc: 'HTTP method (network)'
270
+ option :views, type: :string, desc: 'Views JSON array (defaults to ["summary"])'
271
+ QueryOptions.raw_filters(self)
272
+ def group
273
+ Commands::Query.new(options).apm_group
274
+ end
275
+
276
+ desc 'occurrence', 'Inspect APM occurrences in a group'
277
+ long_desc(<<~DESC, wrap: false)
278
+ Inspect APM occurrences in a group. --metric and --selector
279
+ (worst/by_token/list) are required; identify the group with --group-uuid or
280
+ --group-url. For by_token pass --token; for list paginate with
281
+ --current-token / --direction / --limit.
282
+
283
+ --filters is a JSON object; keys vary by metric and include date_ms,
284
+ app_version, platform ["ios","android"], device, os_version, country,
285
+ carrier, radio, failure_name, failure_type, response_time_ms,
286
+ latency_percentile, experiment. See the Luciq API docs for the full schema.
287
+ DESC
288
+ QueryOptions.app(self)
289
+ option :metric, type: :string, required: true, enum: METRICS, desc: 'APM metric'
290
+ option :selector, type: :string, required: true, enum: %w[worst by_token list], desc: 'Occurrence selector'
291
+ option :group_uuid, type: :string, desc: 'Group UUID (or use --group-url)'
292
+ option :group_url, type: :string, desc: 'Group URL (or use --group-uuid)'
293
+ option :method, type: :string, enum: %w[GET POST PUT PATCH DELETE HEAD OPTIONS], desc: 'HTTP method (network)'
294
+ option :token, type: :string, desc: 'Occurrence token (for by_token)'
295
+ option :current_token, type: :string, desc: 'Pagination cursor token (for list)'
296
+ option :direction, type: :string, enum: %w[first last], desc: 'Pagination direction (for list)'
297
+ option :limit, type: :numeric, desc: 'Maximum number of occurrences (for list)'
298
+ QueryOptions.raw_filters(self)
299
+ def occurrence
300
+ Commands::Query.new(options).apm_occurrence
301
+ end
302
+
303
+ desc 'funnel-events', 'List pickable events for building funnels'
304
+ long_desc(<<~DESC, wrap: false)
305
+ List events (network / screen_loading groups) that can be added to a funnel.
306
+ Narrow with --event-type and/or a name substring via --q.
307
+ DESC
308
+ QueryOptions.app(self)
309
+ option :event_type, type: :string, enum: %w[network screen_loading], desc: 'Event type (default: all)'
310
+ option :q, type: :string, desc: 'Case-insensitive name substring'
311
+ option :limit, type: :numeric, desc: 'Max events per type (1-25, default 20)'
312
+ def funnel_events
313
+ Commands::Query.new(options).apm_funnel_events
314
+ end
315
+
316
+ desc 'funnel-create', 'Create a funnel'
317
+ long_desc(<<~DESC, wrap: false)
318
+ Create a funnel from 2-20 events. --events is a JSON array; each step is one of:
319
+ {"type":"network","ulid":"<event-ulid>"}
320
+ {"type":"screen_loading","ulid":"<event-ulid>"}
321
+ {"type":"user_event","name":"<event-name>"}
322
+ DESC
323
+ QueryOptions.app(self)
324
+ option :name, type: :string, required: true, desc: 'Funnel name'
325
+ option :events, type: :string, required: true, desc: 'Steps JSON array (2-20 items)'
326
+ def funnel_create
327
+ Commands::Query.new(options).apm_funnel_create
328
+ end
329
+
330
+ desc 'funnel-update', 'Update a funnel'
331
+ long_desc(<<~DESC, wrap: false)
332
+ Update a funnel. --events (a JSON array of steps, same shape as funnel-create)
333
+ replaces the funnel's full step set.
334
+ DESC
335
+ QueryOptions.app(self)
336
+ option :ulid, type: :string, required: true, desc: 'Funnel ULID'
337
+ option :name, type: :string, desc: 'Funnel name'
338
+ option :events, type: :string, desc: 'Steps JSON array (replaces the full set)'
339
+ def funnel_update
340
+ Commands::Query.new(options).apm_funnel_update
341
+ end
342
+
343
+ desc 'funnel-delete', 'Delete a funnel'
344
+ QueryOptions.app(self)
345
+ option :ulid, type: :string, required: true, desc: 'Funnel ULID'
346
+ def funnel_delete
347
+ Commands::Query.new(options).apm_funnel_delete
348
+ end
349
+ end
350
+
351
+ class ReviewsCLI < Thor
352
+ desc 'list', 'List reviews for an application'
353
+ long_desc(<<~DESC, wrap: false)
354
+ List reviews for an application.
355
+
356
+ Typed flags: --rating (1-5), --country, --os (ios/android), --sort-by (date),
357
+ --sort-direction. Other filters via --filters:
358
+ date_ms {"gte": <ms>, "lte": <ms>}
359
+ app_version ["1.2.3", ...]
360
+ prompt_type ["custom","native","app_store"]
361
+ DESC
362
+ QueryOptions.app(self)
363
+ QueryOptions.page(self)
364
+ option :sort_by, type: :string, enum: %w[date], desc: 'Sort field'
365
+ option :sort_direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
366
+ option :rating, type: :array, enum: %w[1 2 3 4 5], desc: 'Filter by rating(s) 1-5'
367
+ option :country, type: :array, desc: 'Filter by country/countries'
368
+ option :os, type: :array, enum: %w[ios android], desc: 'Filter by OS'
369
+ QueryOptions.raw_filters(self)
370
+ def list
371
+ Commands::Query.new(options).reviews_list
372
+ end
373
+ end
374
+
375
+ class SurveysCLI < Thor
376
+ desc 'list', 'List surveys for an application'
377
+ long_desc(<<~DESC, wrap: false)
378
+ List surveys for an application.
379
+
380
+ Typed flags --type (0=custom 1=nps 2=app_store) and --status (0=draft
381
+ 1=published 2=paused). The raw --filters object accepts the same keys:
382
+ {"type": [0,1,2], "status": [0,1,2]}.
383
+ DESC
384
+ QueryOptions.app(self)
385
+ QueryOptions.page(self)
386
+ option :type, type: :array, enum: %w[0 1 2], desc: 'Filter by type (0=custom 1=nps 2=app_store)'
387
+ option :status, type: :array, enum: %w[0 1 2], desc: 'Filter by status (0=draft 1=published 2=paused)'
388
+ QueryOptions.raw_filters(self)
389
+ def list
390
+ Commands::Query.new(options).surveys_list
391
+ end
392
+
393
+ desc 'show', 'Show a survey\'s details and response statistics'
394
+ long_desc(<<~DESC, wrap: false)
395
+ Show a survey's questions and response statistics. Paginate responses with --page.
396
+
397
+ Filters via --filters:
398
+ date_ms {"gte": <ms>, "lte": <ms>}
399
+ search_words "<free text>"
400
+ response_status [0,1]
401
+ nps <0-10>
402
+ locale "<locale>"
403
+ app_versions ["1.2.3", ...]
404
+ devices ["iPhone15,2", ...]
405
+ os_versions ["17.4", ...]
406
+ countries ["US", ...]
407
+ platforms ["ios","android"]
408
+ DESC
409
+ QueryOptions.app(self)
410
+ option :id, type: :numeric, required: true, desc: 'Survey id'
411
+ option :page, type: :numeric, desc: 'Responses page number'
412
+ QueryOptions.raw_filters(self)
413
+ def show
414
+ Commands::Query.new(options).survey_show(options[:id])
415
+ end
416
+ end
417
+
418
+ class AppsCLI < Thor
419
+ desc 'list', 'List applications accessible to the authenticated developer'
420
+ QueryOptions.page(self)
421
+ option :platform, type: :string, enum: %w[ios android react_native flutter], desc: 'Filter by platform'
422
+ def list
423
+ Commands::Query.new(options).apps_list
424
+ end
425
+ end
426
+
427
+ class IssuesCLI < Thor
428
+ desc 'list', 'List issues across sources ranked by impact'
429
+ long_desc(<<~DESC, wrap: false)
430
+ List issues across crashes, APM, AI, and bugs, ranked by Apdex impact.
431
+
432
+ Typed flags: --limit, --sort-by (apdex_impact/occurrences_counter),
433
+ --sort-direction, --top-issues (curated shortlist). --pagination is a JSON
434
+ object of per-source cursor tokens. Other filters via --filters:
435
+ date_ms {"gte": <ms>, "lte": <ms>} (>= 24h span)
436
+ search_tokens ["<text>", ...]
437
+ app_version ["1.2.3", ...]
438
+ teams ["<team-id>", ...]
439
+ platform ["IOS","ANDROID","DART","JAVASCRIPT"]
440
+ apm_types ["networks","traces","launches","screen_loadings","frame_drops"]
441
+ crashes_types ["CRASH","ANR","OOM","NON_FATAL"]
442
+ ai_issues_types ["visual_issue","broken_functionality"]
443
+ bugs_types ["<type>", ...]
444
+ apdex_severity ["high","medium","low","no_impact"]
445
+ DESC
446
+ QueryOptions.app(self)
447
+ option :limit, type: :numeric, desc: 'Maximum number of issues (1-50)'
448
+ option :sort_by, type: :string, enum: %w[apdex_impact occurrences_counter], desc: 'Sort field'
449
+ option :sort_direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
450
+ option :top_issues, type: :boolean, desc: 'Return a curated shortlist'
451
+ option :pagination, type: :string, desc: 'Per-source cursor tokens JSON object'
452
+ QueryOptions.raw_filters(self)
453
+ def list
454
+ Commands::Query.new(options).issues_list
455
+ end
456
+ end
457
+
458
+ class OpportunitiesCLI < Thor
459
+ desc 'list', 'List opportunities for an application'
460
+ long_desc(<<~DESC, wrap: false)
461
+ List opportunities ranked by priority then recency.
462
+
463
+ Typed flags --status, --priority, --team-id map into the same filter keys:
464
+ status ["open","in_progress","closed","dismissed"]
465
+ priority ["1","2","3","4","unset"]
466
+ team_id "<team-ulid>" | "unassigned"
467
+ DESC
468
+ QueryOptions.app(self)
469
+ QueryOptions.page(self)
470
+ option :status, type: :array, enum: %w[open in_progress closed dismissed], desc: 'Filter by status'
471
+ option :priority, type: :array, enum: %w[1 2 3 4 unset], desc: 'Filter by priority'
472
+ option :team_id, type: :string, desc: 'Filter by team ULID or "unassigned"'
473
+ QueryOptions.raw_filters(self)
474
+ def list
475
+ Commands::Query.new(options).opportunities_list
476
+ end
477
+
478
+ desc 'show', 'Show details for a single opportunity'
479
+ QueryOptions.app(self)
480
+ option :id, type: :numeric, required: true, desc: 'Opportunity id'
481
+ def show
482
+ Commands::Query.new(options).opportunity_show(options[:id])
483
+ end
484
+ end
485
+
486
+ class AlertsCLI < Thor
487
+ desc 'list', 'List alert rules'
488
+ QueryOptions.app(self)
489
+ option :sort_by, type: :string,
490
+ enum: %w[latest_creation_date last_edit_date highest_triggered_count],
491
+ desc: 'Sort field'
492
+ option :sort_direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
493
+ def list
494
+ Commands::Query.new(options).alerts_list
495
+ end
496
+
497
+ desc 'show', 'Show a single alert rule'
498
+ QueryOptions.app(self)
499
+ option :ulid, type: :string, required: true, desc: 'Alert ULID (e.g. crashes_01HX...)'
500
+ def show
501
+ Commands::Query.new(options).alert_show(options[:ulid])
502
+ end
503
+
504
+ desc 'init', 'Show the menu (types, triggers, conditions, actions) for building rules'
505
+ QueryOptions.app(self)
506
+ def init
507
+ Commands::Query.new(options).alerts_init
508
+ end
509
+
510
+ desc 'create', 'Create an alert rule'
511
+ long_desc(<<~DESC, wrap: false)
512
+ Create an alert rule. Run `luciq alerts init` first for the valid types,
513
+ triggers, conditions, and actions, then pass the rule body as --payload JSON:
514
+ {"type":"Crashes","trigger":"<trigger>","title":"...","operation":0,
515
+ "conditions":[...],"actions":[...],"rule_owner":"<team-id>"}
516
+ DESC
517
+ QueryOptions.app(self)
518
+ option :payload, type: :string, required: true, desc: 'Rule body JSON object'
519
+ def create
520
+ Commands::Query.new(options).alert_create
521
+ end
522
+
523
+ desc 'update', 'Update an alert rule'
524
+ QueryOptions.app(self)
525
+ option :ulid, type: :string, required: true, desc: 'Alert ULID'
526
+ option :payload, type: :string, required: true, desc: 'Rule body JSON object'
527
+ def update
528
+ Commands::Query.new(options).alert_update(options[:ulid])
529
+ end
530
+
531
+ desc 'delete', 'Delete an alert rule'
532
+ QueryOptions.app(self)
533
+ option :ulid, type: :string, required: true, desc: 'Alert ULID'
534
+ def delete
535
+ Commands::Query.new(options).alert_delete(options[:ulid])
536
+ end
537
+ end
538
+
539
+ class IncidentsCLI < Thor
540
+ desc 'list', 'List triggered alerts (incidents)'
541
+ long_desc(<<~DESC, wrap: false)
542
+ List triggered alerts. Typed flags: --sort-by, --sort-direction, --status,
543
+ --type. Other filters via --filters:
544
+ date_ms {"gte": <ms>, "lte": <ms>}
545
+ title ["<token>", ...]
546
+ Status: open, manual_resolve, automatic_resolve.
547
+ Type: overall_app, launch, screen_loading, network, trace, frame_drop, crash,
548
+ anr, oom, non_fatal, fatal_ui_hang, feature_experiment.
549
+ DESC
550
+ QueryOptions.app(self)
551
+ QueryOptions.page(self)
552
+ option :sort_by, type: :string, enum: %w[first_triggered last_triggered count], desc: 'Sort field'
553
+ option :sort_direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
554
+ option :status, type: :array, enum: %w[open manual_resolve automatic_resolve], desc: 'Filter by status'
555
+ option :type, type: :array,
556
+ enum: %w[overall_app launch screen_loading network trace frame_drop crash anr oom
557
+ non_fatal fatal_ui_hang feature_experiment],
558
+ desc: 'Filter by incident type(s)'
559
+ QueryOptions.raw_filters(self)
560
+ def list
561
+ Commands::Query.new(options).incidents_list
562
+ end
563
+
564
+ desc 'show', 'Show a single triggered alert'
565
+ QueryOptions.app(self)
566
+ option :ulid, type: :string, required: true, desc: 'Incident ULID'
567
+ def show
568
+ Commands::Query.new(options).incident_show(options[:ulid])
569
+ end
570
+
571
+ desc 'resolve', 'Resolve a triggered alert'
572
+ QueryOptions.app(self)
573
+ option :ulid, type: :string, required: true, desc: 'Incident ULID'
574
+ def resolve
575
+ Commands::Query.new(options).incident_resolve(options[:ulid])
576
+ end
577
+
578
+ desc 'reopen', 'Reopen a triggered alert'
579
+ QueryOptions.app(self)
580
+ option :ulid, type: :string, required: true, desc: 'Incident ULID'
581
+ def reopen
582
+ Commands::Query.new(options).incident_reopen(options[:ulid])
583
+ end
584
+ end
585
+ end