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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +212 -4
- data/lib/luciq/api/client.rb +159 -10
- data/lib/luciq/cli.rb +46 -17
- data/lib/luciq/commands/query.rb +228 -0
- data/lib/luciq/commands/query_arguments.rb +166 -0
- data/lib/luciq/commands/upload.rb +167 -12
- data/lib/luciq/config.rb +2 -1
- data/lib/luciq/query_cli.rb +579 -0
- data/lib/luciq/upload_cli.rb +188 -0
- data/lib/luciq/version.rb +1 -1
- metadata +8 -4
|
@@ -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
|