luciq-cli 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.
@@ -0,0 +1,579 @@
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
+ Mark a duplicate: --duplicate-of <master-number> (implies --action mark_as_duplicate).
203
+ Detach a duplicate: --action unmark_as_duplicate.
204
+ DESC
205
+ QueryOptions.app(self)
206
+ option :number, type: :numeric, required: true, desc: 'Bug number'
207
+ option :status, type: :string, enum: %w[new closed in_progress], desc: 'New status'
208
+ option :priority, type: :string, enum: %w[na trivial minor major blocker], desc: 'New priority'
209
+ option :tags, type: :array, desc: 'Tags to set (replaces all existing tags)'
210
+ option :clear_tags, type: :boolean, desc: 'Remove all tags'
211
+ option :duplicate_of, type: :numeric, desc: 'Master bug number (marks this a duplicate)'
212
+ option :action, type: :string, enum: %w[mark_as_duplicate unmark_as_duplicate],
213
+ desc: 'Duplicate action (defaults to mark_as_duplicate with --duplicate-of)'
214
+ def update
215
+ Commands::Query.new(options).bug_update(options[:number])
216
+ end
217
+ end
218
+
219
+ class ApmCLI < Thor
220
+ METRICS = %w[network launch flows screen_loading frame_drop].freeze
221
+ GROUP_METRICS = (METRICS + %w[funnels]).freeze
222
+
223
+ desc 'groups', 'Rank APM groups worst-first for an application'
224
+ long_desc(<<~DESC, wrap: false)
225
+ Rank APM groups worst-first. --metric is required (network, launch, flows,
226
+ screen_loading, frame_drop, funnels).
227
+
228
+ --sort is a JSON object {"by": <field>, "direction": "asc"|"desc"}; the CLI
229
+ wraps it in the single-item array the API expects.
230
+ by: failure_rate, p95, p50, apdex, apdex_change, occurrences, dissat_count,
231
+ frozen_frames_percent, slow_frames_percent, count, conversion, drop_off, median_time
232
+
233
+ --filters is a JSON object. Common keys: date_ms {"gte","lte"},
234
+ app_version ["1.2.3"], platform ["ios","android"], group_name, key_metric,
235
+ count, dissat_count, apdex, apdex_change, teams. Available filters vary by
236
+ --metric; see the Luciq API docs for the full per-metric set.
237
+ DESC
238
+ QueryOptions.app(self)
239
+ option :metric, type: :string, required: true, enum: GROUP_METRICS, desc: 'APM metric'
240
+ QueryOptions.page(self)
241
+ option :sort, type: :string, desc: 'Sort JSON object {"by":...,"direction":...} (wrapped in a 1-item array)'
242
+ QueryOptions.raw_filters(self)
243
+ def groups
244
+ Commands::Query.new(options).apm_groups
245
+ end
246
+
247
+ desc 'group', 'Fetch APM panels for a single group'
248
+ long_desc(<<~DESC, wrap: false)
249
+ Fetch APM panels (charts/tables/summary) for a single group. --metric is
250
+ required; identify the group with --group-uuid or --group-url. --method
251
+ applies to the network metric.
252
+
253
+ --views is a JSON array selecting which panels/tables to return (defaults to ["summary"]).
254
+ --filters is a JSON object; keys vary by metric and include date_ms,
255
+ app_version, platform ["ios","android"], device, os_version, country,
256
+ carrier, radio, failure_name, failure_type, response_time_ms,
257
+ custom_attributes, experiment. See the Luciq API docs for the full schema.
258
+ DESC
259
+ QueryOptions.app(self)
260
+ option :metric, type: :string, required: true, enum: GROUP_METRICS, desc: 'APM metric'
261
+ option :group_uuid, type: :string, desc: 'Group UUID (or use --group-url)'
262
+ option :group_url, type: :string, desc: 'Group URL (or use --group-uuid)'
263
+ option :method, type: :string, enum: %w[GET POST PUT PATCH DELETE HEAD OPTIONS], desc: 'HTTP method (network)'
264
+ option :views, type: :string, desc: 'Views JSON array (defaults to ["summary"])'
265
+ QueryOptions.raw_filters(self)
266
+ def group
267
+ Commands::Query.new(options).apm_group
268
+ end
269
+
270
+ desc 'occurrence', 'Inspect APM occurrences in a group'
271
+ long_desc(<<~DESC, wrap: false)
272
+ Inspect APM occurrences in a group. --metric and --selector
273
+ (worst/by_token/list) are required; identify the group with --group-uuid or
274
+ --group-url. For by_token pass --token; for list paginate with
275
+ --current-token / --direction / --limit.
276
+
277
+ --filters is a JSON object; keys vary by metric and include date_ms,
278
+ app_version, platform ["ios","android"], device, os_version, country,
279
+ carrier, radio, failure_name, failure_type, response_time_ms,
280
+ latency_percentile, experiment. See the Luciq API docs for the full schema.
281
+ DESC
282
+ QueryOptions.app(self)
283
+ option :metric, type: :string, required: true, enum: METRICS, desc: 'APM metric'
284
+ option :selector, type: :string, required: true, enum: %w[worst by_token list], desc: 'Occurrence selector'
285
+ option :group_uuid, type: :string, desc: 'Group UUID (or use --group-url)'
286
+ option :group_url, type: :string, desc: 'Group URL (or use --group-uuid)'
287
+ option :method, type: :string, enum: %w[GET POST PUT PATCH DELETE HEAD OPTIONS], desc: 'HTTP method (network)'
288
+ option :token, type: :string, desc: 'Occurrence token (for by_token)'
289
+ option :current_token, type: :string, desc: 'Pagination cursor token (for list)'
290
+ option :direction, type: :string, enum: %w[first last], desc: 'Pagination direction (for list)'
291
+ option :limit, type: :numeric, desc: 'Maximum number of occurrences (for list)'
292
+ QueryOptions.raw_filters(self)
293
+ def occurrence
294
+ Commands::Query.new(options).apm_occurrence
295
+ end
296
+
297
+ desc 'funnel-events', 'List pickable events for building funnels'
298
+ long_desc(<<~DESC, wrap: false)
299
+ List events (network / screen_loading groups) that can be added to a funnel.
300
+ Narrow with --event-type and/or a name substring via --q.
301
+ DESC
302
+ QueryOptions.app(self)
303
+ option :event_type, type: :string, enum: %w[network screen_loading], desc: 'Event type (default: all)'
304
+ option :q, type: :string, desc: 'Case-insensitive name substring'
305
+ option :limit, type: :numeric, desc: 'Max events per type (1-25, default 20)'
306
+ def funnel_events
307
+ Commands::Query.new(options).apm_funnel_events
308
+ end
309
+
310
+ desc 'funnel-create', 'Create a funnel'
311
+ long_desc(<<~DESC, wrap: false)
312
+ Create a funnel from 2-20 events. --events is a JSON array; each step is one of:
313
+ {"type":"network","ulid":"<event-ulid>"}
314
+ {"type":"screen_loading","ulid":"<event-ulid>"}
315
+ {"type":"user_event","name":"<event-name>"}
316
+ DESC
317
+ QueryOptions.app(self)
318
+ option :name, type: :string, required: true, desc: 'Funnel name'
319
+ option :events, type: :string, required: true, desc: 'Steps JSON array (2-20 items)'
320
+ def funnel_create
321
+ Commands::Query.new(options).apm_funnel_create
322
+ end
323
+
324
+ desc 'funnel-update', 'Update a funnel'
325
+ long_desc(<<~DESC, wrap: false)
326
+ Update a funnel. --events (a JSON array of steps, same shape as funnel-create)
327
+ replaces the funnel's full step set.
328
+ DESC
329
+ QueryOptions.app(self)
330
+ option :ulid, type: :string, required: true, desc: 'Funnel ULID'
331
+ option :name, type: :string, desc: 'Funnel name'
332
+ option :events, type: :string, desc: 'Steps JSON array (replaces the full set)'
333
+ def funnel_update
334
+ Commands::Query.new(options).apm_funnel_update
335
+ end
336
+
337
+ desc 'funnel-delete', 'Delete a funnel'
338
+ QueryOptions.app(self)
339
+ option :ulid, type: :string, required: true, desc: 'Funnel ULID'
340
+ def funnel_delete
341
+ Commands::Query.new(options).apm_funnel_delete
342
+ end
343
+ end
344
+
345
+ class ReviewsCLI < Thor
346
+ desc 'list', 'List reviews for an application'
347
+ long_desc(<<~DESC, wrap: false)
348
+ List reviews for an application.
349
+
350
+ Typed flags: --rating (1-5), --country, --os (ios/android), --sort-by (date),
351
+ --sort-direction. Other filters via --filters:
352
+ date_ms {"gte": <ms>, "lte": <ms>}
353
+ app_version ["1.2.3", ...]
354
+ prompt_type ["custom","native","app_store"]
355
+ DESC
356
+ QueryOptions.app(self)
357
+ QueryOptions.page(self)
358
+ option :sort_by, type: :string, enum: %w[date], desc: 'Sort field'
359
+ option :sort_direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
360
+ option :rating, type: :array, enum: %w[1 2 3 4 5], desc: 'Filter by rating(s) 1-5'
361
+ option :country, type: :array, desc: 'Filter by country/countries'
362
+ option :os, type: :array, enum: %w[ios android], desc: 'Filter by OS'
363
+ QueryOptions.raw_filters(self)
364
+ def list
365
+ Commands::Query.new(options).reviews_list
366
+ end
367
+ end
368
+
369
+ class SurveysCLI < Thor
370
+ desc 'list', 'List surveys for an application'
371
+ long_desc(<<~DESC, wrap: false)
372
+ List surveys for an application.
373
+
374
+ Typed flags --type (0=custom 1=nps 2=app_store) and --status (0=draft
375
+ 1=published 2=paused). The raw --filters object accepts the same keys:
376
+ {"type": [0,1,2], "status": [0,1,2]}.
377
+ DESC
378
+ QueryOptions.app(self)
379
+ QueryOptions.page(self)
380
+ option :type, type: :array, enum: %w[0 1 2], desc: 'Filter by type (0=custom 1=nps 2=app_store)'
381
+ option :status, type: :array, enum: %w[0 1 2], desc: 'Filter by status (0=draft 1=published 2=paused)'
382
+ QueryOptions.raw_filters(self)
383
+ def list
384
+ Commands::Query.new(options).surveys_list
385
+ end
386
+
387
+ desc 'show', 'Show a survey\'s details and response statistics'
388
+ long_desc(<<~DESC, wrap: false)
389
+ Show a survey's questions and response statistics. Paginate responses with --page.
390
+
391
+ Filters via --filters:
392
+ date_ms {"gte": <ms>, "lte": <ms>}
393
+ search_words "<free text>"
394
+ response_status [0,1]
395
+ nps <0-10>
396
+ locale "<locale>"
397
+ app_versions ["1.2.3", ...]
398
+ devices ["iPhone15,2", ...]
399
+ os_versions ["17.4", ...]
400
+ countries ["US", ...]
401
+ platforms ["ios","android"]
402
+ DESC
403
+ QueryOptions.app(self)
404
+ option :id, type: :numeric, required: true, desc: 'Survey id'
405
+ option :page, type: :numeric, desc: 'Responses page number'
406
+ QueryOptions.raw_filters(self)
407
+ def show
408
+ Commands::Query.new(options).survey_show(options[:id])
409
+ end
410
+ end
411
+
412
+ class AppsCLI < Thor
413
+ desc 'list', 'List applications accessible to the authenticated developer'
414
+ QueryOptions.page(self)
415
+ option :platform, type: :string, enum: %w[ios android react_native flutter], desc: 'Filter by platform'
416
+ def list
417
+ Commands::Query.new(options).apps_list
418
+ end
419
+ end
420
+
421
+ class IssuesCLI < Thor
422
+ desc 'list', 'List issues across sources ranked by impact'
423
+ long_desc(<<~DESC, wrap: false)
424
+ List issues across crashes, APM, AI, and bugs, ranked by Apdex impact.
425
+
426
+ Typed flags: --limit, --sort-by (apdex_impact/occurrences_counter),
427
+ --sort-direction, --top-issues (curated shortlist). --pagination is a JSON
428
+ object of per-source cursor tokens. Other filters via --filters:
429
+ date_ms {"gte": <ms>, "lte": <ms>} (>= 24h span)
430
+ search_tokens ["<text>", ...]
431
+ app_version ["1.2.3", ...]
432
+ teams ["<team-id>", ...]
433
+ platform ["IOS","ANDROID","DART","JAVASCRIPT"]
434
+ apm_types ["networks","traces","launches","screen_loadings","frame_drops"]
435
+ crashes_types ["CRASH","ANR","OOM","NON_FATAL"]
436
+ ai_issues_types ["visual_issue","broken_functionality"]
437
+ bugs_types ["<type>", ...]
438
+ apdex_severity ["high","medium","low","no_impact"]
439
+ DESC
440
+ QueryOptions.app(self)
441
+ option :limit, type: :numeric, desc: 'Maximum number of issues (1-50)'
442
+ option :sort_by, type: :string, enum: %w[apdex_impact occurrences_counter], desc: 'Sort field'
443
+ option :sort_direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
444
+ option :top_issues, type: :boolean, desc: 'Return a curated shortlist'
445
+ option :pagination, type: :string, desc: 'Per-source cursor tokens JSON object'
446
+ QueryOptions.raw_filters(self)
447
+ def list
448
+ Commands::Query.new(options).issues_list
449
+ end
450
+ end
451
+
452
+ class OpportunitiesCLI < Thor
453
+ desc 'list', 'List opportunities for an application'
454
+ long_desc(<<~DESC, wrap: false)
455
+ List opportunities ranked by priority then recency.
456
+
457
+ Typed flags --status, --priority, --team-id map into the same filter keys:
458
+ status ["open","in_progress","closed","dismissed"]
459
+ priority ["1","2","3","4","unset"]
460
+ team_id "<team-ulid>" | "unassigned"
461
+ DESC
462
+ QueryOptions.app(self)
463
+ QueryOptions.page(self)
464
+ option :status, type: :array, enum: %w[open in_progress closed dismissed], desc: 'Filter by status'
465
+ option :priority, type: :array, enum: %w[1 2 3 4 unset], desc: 'Filter by priority'
466
+ option :team_id, type: :string, desc: 'Filter by team ULID or "unassigned"'
467
+ QueryOptions.raw_filters(self)
468
+ def list
469
+ Commands::Query.new(options).opportunities_list
470
+ end
471
+
472
+ desc 'show', 'Show details for a single opportunity'
473
+ QueryOptions.app(self)
474
+ option :id, type: :numeric, required: true, desc: 'Opportunity id'
475
+ def show
476
+ Commands::Query.new(options).opportunity_show(options[:id])
477
+ end
478
+ end
479
+
480
+ class AlertsCLI < Thor
481
+ desc 'list', 'List alert rules'
482
+ QueryOptions.app(self)
483
+ option :sort_by, type: :string,
484
+ enum: %w[latest_creation_date last_edit_date highest_triggered_count],
485
+ desc: 'Sort field'
486
+ option :sort_direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
487
+ def list
488
+ Commands::Query.new(options).alerts_list
489
+ end
490
+
491
+ desc 'show', 'Show a single alert rule'
492
+ QueryOptions.app(self)
493
+ option :ulid, type: :string, required: true, desc: 'Alert ULID (e.g. crashes_01HX...)'
494
+ def show
495
+ Commands::Query.new(options).alert_show(options[:ulid])
496
+ end
497
+
498
+ desc 'init', 'Show the menu (types, triggers, conditions, actions) for building rules'
499
+ QueryOptions.app(self)
500
+ def init
501
+ Commands::Query.new(options).alerts_init
502
+ end
503
+
504
+ desc 'create', 'Create an alert rule'
505
+ long_desc(<<~DESC, wrap: false)
506
+ Create an alert rule. Run `luciq alerts init` first for the valid types,
507
+ triggers, conditions, and actions, then pass the rule body as --payload JSON:
508
+ {"type":"Crashes","trigger":"<trigger>","title":"...","operation":0,
509
+ "conditions":[...],"actions":[...],"rule_owner":"<team-id>"}
510
+ DESC
511
+ QueryOptions.app(self)
512
+ option :payload, type: :string, required: true, desc: 'Rule body JSON object'
513
+ def create
514
+ Commands::Query.new(options).alert_create
515
+ end
516
+
517
+ desc 'update', 'Update an alert rule'
518
+ QueryOptions.app(self)
519
+ option :ulid, type: :string, required: true, desc: 'Alert ULID'
520
+ option :payload, type: :string, required: true, desc: 'Rule body JSON object'
521
+ def update
522
+ Commands::Query.new(options).alert_update(options[:ulid])
523
+ end
524
+
525
+ desc 'delete', 'Delete an alert rule'
526
+ QueryOptions.app(self)
527
+ option :ulid, type: :string, required: true, desc: 'Alert ULID'
528
+ def delete
529
+ Commands::Query.new(options).alert_delete(options[:ulid])
530
+ end
531
+ end
532
+
533
+ class IncidentsCLI < Thor
534
+ desc 'list', 'List triggered alerts (incidents)'
535
+ long_desc(<<~DESC, wrap: false)
536
+ List triggered alerts. Typed flags: --sort-by, --sort-direction, --status,
537
+ --type. Other filters via --filters:
538
+ date_ms {"gte": <ms>, "lte": <ms>}
539
+ title ["<token>", ...]
540
+ Status: open, manual_resolve, automatic_resolve.
541
+ Type: overall_app, launch, screen_loading, network, trace, frame_drop, crash,
542
+ anr, oom, non_fatal, fatal_ui_hang, feature_experiment.
543
+ DESC
544
+ QueryOptions.app(self)
545
+ QueryOptions.page(self)
546
+ option :sort_by, type: :string, enum: %w[first_triggered last_triggered count], desc: 'Sort field'
547
+ option :sort_direction, type: :string, enum: %w[asc desc], desc: 'Sort direction'
548
+ option :status, type: :array, enum: %w[open manual_resolve automatic_resolve], desc: 'Filter by status'
549
+ option :type, type: :array,
550
+ enum: %w[overall_app launch screen_loading network trace frame_drop crash anr oom
551
+ non_fatal fatal_ui_hang feature_experiment],
552
+ desc: 'Filter by incident type(s)'
553
+ QueryOptions.raw_filters(self)
554
+ def list
555
+ Commands::Query.new(options).incidents_list
556
+ end
557
+
558
+ desc 'show', 'Show a single triggered alert'
559
+ QueryOptions.app(self)
560
+ option :ulid, type: :string, required: true, desc: 'Incident ULID'
561
+ def show
562
+ Commands::Query.new(options).incident_show(options[:ulid])
563
+ end
564
+
565
+ desc 'resolve', 'Resolve a triggered alert'
566
+ QueryOptions.app(self)
567
+ option :ulid, type: :string, required: true, desc: 'Incident ULID'
568
+ def resolve
569
+ Commands::Query.new(options).incident_resolve(options[:ulid])
570
+ end
571
+
572
+ desc 'reopen', 'Reopen a triggered alert'
573
+ QueryOptions.app(self)
574
+ option :ulid, type: :string, required: true, desc: 'Incident ULID'
575
+ def reopen
576
+ Commands::Query.new(options).incident_reopen(options[:ulid])
577
+ end
578
+ end
579
+ end