@tenderprompt/cli 0.1.9 → 0.1.11
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.
- package/README.md +50 -2
- package/dist/index.js +1397 -87
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,14 @@ class TenderCliUsageError extends Error {
|
|
|
39
39
|
this.name = "TenderCliUsageError";
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
class TenderCliAnalyticsApiError extends Error {
|
|
43
|
+
reasonCode;
|
|
44
|
+
constructor(message, reasonCode) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.reasonCode = reasonCode;
|
|
47
|
+
this.name = "TenderCliAnalyticsApiError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
42
50
|
const DEFAULT_PROFILE_NAME = "default";
|
|
43
51
|
const DEFAULT_BASE_URL = "https://app.tenderprompt.com";
|
|
44
52
|
const ARTIFACT_SOURCE_EXPORT_MAX_FILES = 1000;
|
|
@@ -199,6 +207,16 @@ Commands:
|
|
|
199
207
|
List or create saved dashboards
|
|
200
208
|
tender app analytics charts create <app-id>
|
|
201
209
|
Create a saved chart from a chart spec
|
|
210
|
+
tender app analytics charts list <app-id>
|
|
211
|
+
List saved charts in one dashboard
|
|
212
|
+
tender app analytics charts update <app-id>
|
|
213
|
+
Update a saved chart title or spec
|
|
214
|
+
tender app analytics charts delete <app-id>
|
|
215
|
+
Delete a saved chart
|
|
216
|
+
tender app analytics property-slots list <app-id>
|
|
217
|
+
List promoted property slots
|
|
218
|
+
tender app analytics property-slots activate <app-id>
|
|
219
|
+
Activate a promoted property slot
|
|
202
220
|
tender app context fetch <app-id>
|
|
203
221
|
Fetch AGENTS.md, skills, and Tender App scaffold files
|
|
204
222
|
tender app context status <app-id>
|
|
@@ -234,6 +252,11 @@ Examples:
|
|
|
234
252
|
tender app analytics dashboards artifact_123 --json
|
|
235
253
|
tender app analytics dashboards artifact_123 --create "Agent dashboard" --json
|
|
236
254
|
tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json
|
|
255
|
+
tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json
|
|
256
|
+
tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --spec funnel.json --dry-run --json
|
|
257
|
+
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
|
|
258
|
+
tender app analytics property-slots list artifact_123 --json
|
|
259
|
+
tender app analytics property-slots activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
|
|
237
260
|
tender app context fetch artifact_123 --dir ./widget --dry-run --json
|
|
238
261
|
tender app context status artifact_123 --dir ./widget --json
|
|
239
262
|
tender app context refresh artifact_123 --dir ./widget --dry-run --json
|
|
@@ -266,7 +289,13 @@ Commands:
|
|
|
266
289
|
tender app analytics suggestions <app-id> Generate starter chart suggestions
|
|
267
290
|
tender app analytics dashboards <app-id> List or create dashboards
|
|
268
291
|
tender app analytics export <kind> <app-id> Export catalog metadata or query rows
|
|
292
|
+
tender app analytics charts templates Print reusable chart spec templates
|
|
269
293
|
tender app analytics charts create <app-id> Create a saved chart
|
|
294
|
+
tender app analytics charts list <app-id> List saved charts in one dashboard
|
|
295
|
+
tender app analytics charts update <app-id> Update a saved chart
|
|
296
|
+
tender app analytics charts delete <app-id> Delete a saved chart
|
|
297
|
+
tender app analytics property-slots list <app-id> List promoted property slots
|
|
298
|
+
tender app analytics property-slots activate <app-id> Activate a promoted property slot
|
|
270
299
|
|
|
271
300
|
Examples:
|
|
272
301
|
tender app analytics summary artifact_123 --range 30d --json
|
|
@@ -277,7 +306,13 @@ Examples:
|
|
|
277
306
|
tender app analytics export query artifact_123 --spec chart.json --format csv
|
|
278
307
|
tender app analytics suggestions artifact_123 --range 2026-04-01..2026-04-30 --json
|
|
279
308
|
tender app analytics dashboards artifact_123 --create "Agent dashboard" --json
|
|
280
|
-
tender app analytics charts
|
|
309
|
+
tender app analytics charts templates --json
|
|
310
|
+
tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json
|
|
311
|
+
tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json
|
|
312
|
+
tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --spec funnel.json --dry-run --json
|
|
313
|
+
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
|
|
314
|
+
tender app analytics property-slots list artifact_123 --json
|
|
315
|
+
tender app analytics property-slots activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json`;
|
|
281
316
|
}
|
|
282
317
|
function analyticsSummaryHelp() {
|
|
283
318
|
return `Usage: tender app analytics summary <app-id> [--range <7d|30d|90d|YYYY-MM-DD..YYYY-MM-DD>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
@@ -340,19 +375,72 @@ Examples:
|
|
|
340
375
|
tender app analytics dashboards artifact_123 --create "Agent dashboard" --dry-run --json
|
|
341
376
|
tender app analytics dashboards artifact_123 --create "Agent dashboard" --json`;
|
|
342
377
|
}
|
|
378
|
+
function analyticsChartsTemplatesHelp() {
|
|
379
|
+
return `Usage: tender app analytics charts templates [--json]
|
|
380
|
+
|
|
381
|
+
Print reusable chart spec templates for agents. Adapt templates to observed
|
|
382
|
+
events, paths, and property keys from analytics capabilities before querying or
|
|
383
|
+
saving.
|
|
384
|
+
|
|
385
|
+
Examples:
|
|
386
|
+
tender app analytics charts templates --json`;
|
|
387
|
+
}
|
|
343
388
|
function analyticsChartsCreateHelp() {
|
|
344
389
|
return `Usage: tender app analytics charts create <app-id> --dashboard <dashboard-id> --title <title> --spec <file|-> [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
345
390
|
|
|
391
|
+
Property charts automatically activate required promoted property slots before
|
|
392
|
+
saving when the spec has an event_name = filter.
|
|
393
|
+
|
|
346
394
|
Examples:
|
|
347
395
|
tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --dry-run --json
|
|
348
396
|
tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json`;
|
|
349
397
|
}
|
|
398
|
+
function analyticsChartsListHelp() {
|
|
399
|
+
return `Usage: tender app analytics charts list <app-id> --dashboard <dashboard-id> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
400
|
+
|
|
401
|
+
Examples:
|
|
402
|
+
tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json`;
|
|
403
|
+
}
|
|
404
|
+
function analyticsChartsUpdateHelp() {
|
|
405
|
+
return `Usage: tender app analytics charts update <app-id> --chart <chart-id> [--title <title>] [--spec <file|->] [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
406
|
+
|
|
407
|
+
At least one of --title or --spec is required. When one is omitted, the CLI reads
|
|
408
|
+
the existing saved chart and keeps that value.
|
|
409
|
+
Property charts automatically activate required promoted property slots before
|
|
410
|
+
saving when the spec has an event_name = filter.
|
|
411
|
+
|
|
412
|
+
Examples:
|
|
413
|
+
tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --dry-run --json
|
|
414
|
+
tender app analytics charts update artifact_123 --chart analytics_chart_123 --spec funnel.json --dry-run --json
|
|
415
|
+
tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --spec - --json < funnel.json`;
|
|
416
|
+
}
|
|
417
|
+
function analyticsChartsDeleteHelp() {
|
|
418
|
+
return `Usage: tender app analytics charts delete <app-id> --chart <chart-id> [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
419
|
+
|
|
420
|
+
Examples:
|
|
421
|
+
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
|
|
422
|
+
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --json`;
|
|
423
|
+
}
|
|
424
|
+
function analyticsPropertySlotsListHelp() {
|
|
425
|
+
return `Usage: tender app analytics property-slots list <app-id> [--event <event-name>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
426
|
+
|
|
427
|
+
Examples:
|
|
428
|
+
tender app analytics property-slots list artifact_123 --json
|
|
429
|
+
tender app analytics property-slots list artifact_123 --event signup_started --json`;
|
|
430
|
+
}
|
|
431
|
+
function analyticsPropertySlotsActivateHelp() {
|
|
432
|
+
return `Usage: tender app analytics property-slots activate <app-id> --event <event-name> --property <property-key> --type <dimension|metric> [--slot <slot-name>] [--label <label>] [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
433
|
+
|
|
434
|
+
Examples:
|
|
435
|
+
tender app analytics property-slots activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
|
|
436
|
+
tender app analytics property-slots activate artifact_123 --event score_saved --property score --type metric --json`;
|
|
437
|
+
}
|
|
350
438
|
function artifactsListHelp() {
|
|
351
439
|
return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
352
440
|
|
|
353
441
|
Options:
|
|
354
442
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
355
|
-
--token <token> Tender API token.
|
|
443
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
356
444
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
357
445
|
--json Emit stable JSON for coding agents.
|
|
358
446
|
|
|
@@ -369,7 +457,7 @@ Options:
|
|
|
369
457
|
--target-pack <id> Target pack id. Defaults to the API default.
|
|
370
458
|
--target-pack-version <value> Target pack version. Defaults to the API default.
|
|
371
459
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
372
|
-
--token <token> Tender API token.
|
|
460
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
373
461
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
374
462
|
--json Emit stable JSON for coding agents.
|
|
375
463
|
|
|
@@ -383,7 +471,7 @@ function artifactsUpdateHelp() {
|
|
|
383
471
|
Options:
|
|
384
472
|
--name <name> New app name.
|
|
385
473
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
386
|
-
--token <token> Tender API token.
|
|
474
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
387
475
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
388
476
|
--json Emit stable JSON for coding agents.
|
|
389
477
|
|
|
@@ -397,7 +485,7 @@ Options:
|
|
|
397
485
|
--dir <path> Local checkout directory to create/update. Defaults to current directory.
|
|
398
486
|
--remote <name> Git remote name to add or update. Defaults to tender.
|
|
399
487
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
400
|
-
--token <token> Tender API token.
|
|
488
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
401
489
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
402
490
|
--ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
|
|
403
491
|
--dry-run Show file and Git actions without writing.
|
|
@@ -415,7 +503,7 @@ function artifactsContextFetchHelp() {
|
|
|
415
503
|
Options:
|
|
416
504
|
--dir <path> Local checkout to update. Defaults to current directory.
|
|
417
505
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
418
|
-
--token <token> Tender API token.
|
|
506
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
419
507
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
420
508
|
--dry-run Show create/overwrite/conflict actions without writing files.
|
|
421
509
|
--force Overwrite conflicting managed context files.
|
|
@@ -432,7 +520,7 @@ function artifactsContextStatusHelp() {
|
|
|
432
520
|
Options:
|
|
433
521
|
--dir <path> Local checkout to inspect. Defaults to current directory.
|
|
434
522
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
435
|
-
--token <token> Tender API token.
|
|
523
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
436
524
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
437
525
|
--json Emit stable JSON for coding agents.
|
|
438
526
|
|
|
@@ -445,7 +533,7 @@ function artifactsContextRefreshHelp() {
|
|
|
445
533
|
Options:
|
|
446
534
|
--dir <path> Local checkout to update. Defaults to current directory.
|
|
447
535
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
448
|
-
--token <token> Tender API token.
|
|
536
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
449
537
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
450
538
|
--dry-run Show create/overwrite/conflict actions without writing files.
|
|
451
539
|
--force Overwrite conflicting managed context files.
|
|
@@ -502,7 +590,7 @@ function gitRemoteHelp() {
|
|
|
502
590
|
|
|
503
591
|
Options:
|
|
504
592
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
505
|
-
--token <token> Tender API token.
|
|
593
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
506
594
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
507
595
|
--ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
|
|
508
596
|
--json Emit stable JSON for coding agents.
|
|
@@ -519,7 +607,7 @@ Options:
|
|
|
519
607
|
--dir <path> Local Git checkout to configure. Defaults to current directory.
|
|
520
608
|
--remote <name> Git remote name to add or update. Defaults to tender.
|
|
521
609
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
522
|
-
--token <token> Tender API token.
|
|
610
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
523
611
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
524
612
|
--ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
|
|
525
613
|
--dry-run Show Git commands without running them.
|
|
@@ -547,7 +635,7 @@ function previewRebuildHelp() {
|
|
|
547
635
|
Options:
|
|
548
636
|
--commit <sha> Rebuild preview only if the app workspace is at this Git commit.
|
|
549
637
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
550
|
-
--token <token> Tender API token.
|
|
638
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
551
639
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
552
640
|
--stream Request lifecycle events from the API.
|
|
553
641
|
--json Emit stable JSON for coding agents.
|
|
@@ -561,7 +649,7 @@ function validateHelp() {
|
|
|
561
649
|
|
|
562
650
|
Options:
|
|
563
651
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
564
|
-
--token <token> Tender API token.
|
|
652
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
565
653
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
566
654
|
--stream Request lifecycle events from the API.
|
|
567
655
|
--json Emit stable JSON for coding agents.
|
|
@@ -576,7 +664,7 @@ function buildHelp() {
|
|
|
576
664
|
Options:
|
|
577
665
|
--commit <sha> Build only if the app workspace is at this Git commit.
|
|
578
666
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
579
|
-
--token <token> Tender API token.
|
|
667
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
580
668
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
581
669
|
--stream Request lifecycle events from the API.
|
|
582
670
|
--json Emit stable JSON for coding agents.
|
|
@@ -592,7 +680,7 @@ Options:
|
|
|
592
680
|
--commit <sha> Show the latest preview job for one artifact Git commit.
|
|
593
681
|
--job <job-id> Show one lifecycle job directly.
|
|
594
682
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
595
|
-
--token <token> Tender API token.
|
|
683
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
596
684
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
597
685
|
--json Emit stable JSON for coding agents.
|
|
598
686
|
|
|
@@ -607,7 +695,7 @@ Options:
|
|
|
607
695
|
--commit <sha> Watch the latest preview job for one artifact Git commit.
|
|
608
696
|
--job <job-id> Watch one lifecycle job directly.
|
|
609
697
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
610
|
-
--token <token> Tender API token.
|
|
698
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
611
699
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
612
700
|
--stream Request Server-Sent Events from the API.
|
|
613
701
|
--json Emit lifecycle events as JSON lines.
|
|
@@ -625,7 +713,7 @@ Options:
|
|
|
625
713
|
--commit <sha> Publish only if the app workspace is at this Git commit.
|
|
626
714
|
--message <text> Human audit message returned in CLI output.
|
|
627
715
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
628
|
-
--token <token> Tender API token.
|
|
716
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
629
717
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
630
718
|
--stream Request lifecycle events from the API.
|
|
631
719
|
--json Emit stable JSON for coding agents.
|
|
@@ -640,7 +728,7 @@ function publishStatusHelp() {
|
|
|
640
728
|
Options:
|
|
641
729
|
--job <job-id> Show one publish lifecycle job directly.
|
|
642
730
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
643
|
-
--token <token> Tender API token.
|
|
731
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
644
732
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
645
733
|
--json Emit stable JSON for coding agents.
|
|
646
734
|
|
|
@@ -653,7 +741,7 @@ function publishWatchHelp() {
|
|
|
653
741
|
Options:
|
|
654
742
|
--job <job-id> Watch one publish lifecycle job directly.
|
|
655
743
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
656
|
-
--token <token> Tender API token.
|
|
744
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
657
745
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
658
746
|
--stream Request Server-Sent Events from the API.
|
|
659
747
|
--json Emit lifecycle events as JSON lines.
|
|
@@ -725,14 +813,23 @@ function tenderCliCapabilities() {
|
|
|
725
813
|
when: "When understanding app analytics or creating saved charts.",
|
|
726
814
|
commands: [
|
|
727
815
|
"tender app analytics capabilities <artifact-id> --include-catalog --range 30d --json",
|
|
816
|
+
"tender app analytics charts templates --json",
|
|
728
817
|
"tender app analytics query <artifact-id> --spec chart.json --json",
|
|
729
818
|
"tender app analytics export query <artifact-id> --spec chart.json --format csv",
|
|
819
|
+
"tender app analytics property-slots list <artifact-id> --json",
|
|
820
|
+
"tender app analytics property-slots activate <artifact-id> --event <event-name> --property <property-key> --type dimension --dry-run --json",
|
|
730
821
|
"tender app analytics dashboards <artifact-id> --create \"Agent dashboard\" --dry-run --json",
|
|
731
822
|
"tender app analytics charts create <artifact-id> --dashboard <dashboard-id> --title <title> --spec chart.json --dry-run --json",
|
|
823
|
+
"tender app analytics charts list <artifact-id> --dashboard <dashboard-id> --json",
|
|
824
|
+
"tender app analytics charts update <artifact-id> --chart <chart-id> --title <title> --spec chart.json --dry-run --json",
|
|
825
|
+
"tender app analytics charts delete <artifact-id> --chart <chart-id> --dry-run --json",
|
|
732
826
|
],
|
|
733
827
|
notes: [
|
|
734
828
|
"Combine CLI capabilities with local source inspection of analytics calls.",
|
|
735
|
-
"Validate chart specs with query before
|
|
829
|
+
"Validate chart specs with query before creating or updating saved charts.",
|
|
830
|
+
"Property chart create/update commands automatically activate missing promoted property slots when the spec has an event_name = filter.",
|
|
831
|
+
"Ad hoc query is read-only; if it reports a missing property slot, activate the slot first or save the chart through charts create/update.",
|
|
832
|
+
"Use list before writes so agents can avoid duplicates and explain the exact chart they are changing.",
|
|
736
833
|
],
|
|
737
834
|
},
|
|
738
835
|
{
|
|
@@ -782,6 +879,52 @@ function tenderCliCapabilities() {
|
|
|
782
879
|
requiresAuth: true,
|
|
783
880
|
writes: false,
|
|
784
881
|
},
|
|
882
|
+
{
|
|
883
|
+
command: "tender app analytics charts templates --json",
|
|
884
|
+
purpose: "Print reusable chart spec templates for agents to adapt to observed analytics data.",
|
|
885
|
+
requiresAuth: false,
|
|
886
|
+
writes: false,
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
command: "tender app analytics property-slots list <artifact-id> --json",
|
|
890
|
+
purpose: "List active promoted property slots that make custom event properties queryable.",
|
|
891
|
+
requiresAuth: true,
|
|
892
|
+
writes: false,
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
command: "tender app analytics property-slots activate <artifact-id> --event <event-name> --property <property-key> --type <dimension|metric> --dry-run --json",
|
|
896
|
+
purpose: "Preview or activate the promoted slot needed for a property group-by or property metric chart.",
|
|
897
|
+
requiresAuth: true,
|
|
898
|
+
writes: true,
|
|
899
|
+
dryRun: true,
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
command: "tender app analytics charts create <artifact-id> --dashboard <dashboard-id> --title <title> --spec chart.json --dry-run --json",
|
|
903
|
+
purpose: "Preview or create a saved chart from a validated chart spec.",
|
|
904
|
+
requiresAuth: true,
|
|
905
|
+
writes: true,
|
|
906
|
+
dryRun: true,
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
command: "tender app analytics charts list <artifact-id> --dashboard <dashboard-id> --json",
|
|
910
|
+
purpose: "List saved charts and full chart specs from one dashboard.",
|
|
911
|
+
requiresAuth: true,
|
|
912
|
+
writes: false,
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
command: "tender app analytics charts update <artifact-id> --chart <chart-id> --title <title> --spec chart.json --dry-run --json",
|
|
916
|
+
purpose: "Preview or update a saved chart title and chart spec.",
|
|
917
|
+
requiresAuth: true,
|
|
918
|
+
writes: true,
|
|
919
|
+
dryRun: true,
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
command: "tender app analytics charts delete <artifact-id> --chart <chart-id> --dry-run --json",
|
|
923
|
+
purpose: "Preview or delete a saved chart.",
|
|
924
|
+
requiresAuth: true,
|
|
925
|
+
writes: true,
|
|
926
|
+
dryRun: true,
|
|
927
|
+
},
|
|
785
928
|
],
|
|
786
929
|
security: [
|
|
787
930
|
"Prefer artifact-scoped device tokens.",
|
|
@@ -902,6 +1045,19 @@ async function resolveApiCredentials(input) {
|
|
|
902
1045
|
profileName: null,
|
|
903
1046
|
};
|
|
904
1047
|
}
|
|
1048
|
+
if (input.profileName) {
|
|
1049
|
+
const config = await readConfig(input.env);
|
|
1050
|
+
const profile = config.profiles?.[input.profileName];
|
|
1051
|
+
if (!profile) {
|
|
1052
|
+
throw new TenderCliUsageError(`Tender profile "${input.profileName}" was not found. Run \`tender auth login --device --save-profile ${input.profileName}\` or choose another --profile.`);
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
baseUrl: input.baseUrl ?? profile.baseUrl ?? readDefaultBaseUrl(input.env),
|
|
1056
|
+
token: profile.token,
|
|
1057
|
+
source: "config",
|
|
1058
|
+
profileName: input.profileName,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
905
1061
|
if (input.env.TENDER_API_TOKEN) {
|
|
906
1062
|
return {
|
|
907
1063
|
baseUrl: input.baseUrl ?? readDefaultBaseUrl(input.env),
|
|
@@ -911,7 +1067,7 @@ async function resolveApiCredentials(input) {
|
|
|
911
1067
|
};
|
|
912
1068
|
}
|
|
913
1069
|
const config = await readConfig(input.env);
|
|
914
|
-
const profileName =
|
|
1070
|
+
const profileName = config.defaultProfile ?? DEFAULT_PROFILE_NAME;
|
|
915
1071
|
const profile = config.profiles?.[profileName];
|
|
916
1072
|
if (!profile) {
|
|
917
1073
|
throw new TenderCliUsageError("Tender API token is required. Run `tender auth login --device`, set TENDER_API_TOKEN, or pass --token <token>.");
|
|
@@ -2555,6 +2711,21 @@ function parseAnalyticsDashboardsArgs(args) {
|
|
|
2555
2711
|
}
|
|
2556
2712
|
return { command: "app analytics dashboards", artifactId, name, dryRun, ...parsed };
|
|
2557
2713
|
}
|
|
2714
|
+
function parseAnalyticsChartsTemplatesArgs(args) {
|
|
2715
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2716
|
+
return { command: "help", topic: "app analytics charts templates" };
|
|
2717
|
+
}
|
|
2718
|
+
let json = false;
|
|
2719
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2720
|
+
const arg = args[index];
|
|
2721
|
+
if (arg === "--json") {
|
|
2722
|
+
json = true;
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
throw new TenderCliUsageError(`Unknown analytics charts templates option: ${arg}`);
|
|
2726
|
+
}
|
|
2727
|
+
return { command: "app analytics charts templates", json };
|
|
2728
|
+
}
|
|
2558
2729
|
function parseAnalyticsExportCatalogArgs(args) {
|
|
2559
2730
|
if (args.includes("--help") || args.includes("-h")) {
|
|
2560
2731
|
return { command: "help", topic: "app analytics export catalog" };
|
|
@@ -2632,6 +2803,244 @@ function parseAnalyticsExportQueryArgs(args) {
|
|
|
2632
2803
|
...parsed,
|
|
2633
2804
|
};
|
|
2634
2805
|
}
|
|
2806
|
+
function analyticsChartTemplates() {
|
|
2807
|
+
return [
|
|
2808
|
+
{
|
|
2809
|
+
id: "daily_custom_events",
|
|
2810
|
+
title: "Daily custom events",
|
|
2811
|
+
visualization: "line",
|
|
2812
|
+
purpose: "Track app-owned business events over time.",
|
|
2813
|
+
usableAsIs: true,
|
|
2814
|
+
adaptWith: [
|
|
2815
|
+
"Use when the catalog has observed custom events.",
|
|
2816
|
+
"Switch the range preset to 7d or 90d when the user's question asks for that window.",
|
|
2817
|
+
],
|
|
2818
|
+
spec: {
|
|
2819
|
+
version: 1,
|
|
2820
|
+
dataset: "events",
|
|
2821
|
+
visualization: "line",
|
|
2822
|
+
metric: { name: "custom_event_count" },
|
|
2823
|
+
groupBy: [{ field: "day" }],
|
|
2824
|
+
filters: [],
|
|
2825
|
+
range: { preset: "30d" },
|
|
2826
|
+
sort: [{ field: "label", direction: "asc" }],
|
|
2827
|
+
limit: 30,
|
|
2828
|
+
},
|
|
2829
|
+
},
|
|
2830
|
+
{
|
|
2831
|
+
id: "top_custom_events",
|
|
2832
|
+
title: "Top custom events",
|
|
2833
|
+
visualization: "bar",
|
|
2834
|
+
purpose: "Show which app-owned events happen most often.",
|
|
2835
|
+
usableAsIs: true,
|
|
2836
|
+
adaptWith: [
|
|
2837
|
+
"Use this before proposing deeper charts so the agent can see the dominant event shape.",
|
|
2838
|
+
],
|
|
2839
|
+
spec: {
|
|
2840
|
+
version: 1,
|
|
2841
|
+
dataset: "events",
|
|
2842
|
+
visualization: "bar",
|
|
2843
|
+
metric: { name: "custom_event_count" },
|
|
2844
|
+
groupBy: [{ field: "event_name" }],
|
|
2845
|
+
filters: [],
|
|
2846
|
+
range: { preset: "30d" },
|
|
2847
|
+
sort: [{ field: "value", direction: "desc" }],
|
|
2848
|
+
limit: 10,
|
|
2849
|
+
},
|
|
2850
|
+
},
|
|
2851
|
+
{
|
|
2852
|
+
id: "top_request_paths",
|
|
2853
|
+
title: "Top request paths",
|
|
2854
|
+
visualization: "bar",
|
|
2855
|
+
purpose: "Find the generated app routes receiving the most traffic.",
|
|
2856
|
+
usableAsIs: true,
|
|
2857
|
+
adaptWith: [
|
|
2858
|
+
"Use only when the capabilities catalog reports request paths.",
|
|
2859
|
+
"Inspect the local source to understand what the busiest routes represent.",
|
|
2860
|
+
],
|
|
2861
|
+
spec: {
|
|
2862
|
+
version: 1,
|
|
2863
|
+
dataset: "events",
|
|
2864
|
+
visualization: "bar",
|
|
2865
|
+
metric: { name: "request_count" },
|
|
2866
|
+
groupBy: [{ field: "path" }],
|
|
2867
|
+
filters: [],
|
|
2868
|
+
range: { preset: "30d" },
|
|
2869
|
+
sort: [{ field: "value", direction: "desc" }],
|
|
2870
|
+
limit: 10,
|
|
2871
|
+
},
|
|
2872
|
+
},
|
|
2873
|
+
{
|
|
2874
|
+
id: "event_funnel_by_name",
|
|
2875
|
+
title: "Event funnel",
|
|
2876
|
+
visualization: "funnel",
|
|
2877
|
+
purpose: "Measure ordered conversion through observed custom event names.",
|
|
2878
|
+
usableAsIs: false,
|
|
2879
|
+
placeholders: {
|
|
2880
|
+
eventNames: ["<start_event>", "<middle_event>", "<conversion_event>"],
|
|
2881
|
+
},
|
|
2882
|
+
adaptWith: [
|
|
2883
|
+
"Replace placeholders with ordered event names from catalog classification or observed events.",
|
|
2884
|
+
"Use only event names observed for the selected app.",
|
|
2885
|
+
"Validate with analytics query before saving because funnel order changes the meaning.",
|
|
2886
|
+
],
|
|
2887
|
+
spec: {
|
|
2888
|
+
version: 1,
|
|
2889
|
+
dataset: "events",
|
|
2890
|
+
visualization: "funnel",
|
|
2891
|
+
metric: { name: "custom_event_count" },
|
|
2892
|
+
groupBy: [{ field: "event_name" }],
|
|
2893
|
+
filters: [
|
|
2894
|
+
{
|
|
2895
|
+
field: "event_name",
|
|
2896
|
+
op: "in",
|
|
2897
|
+
value: ["<start_event>", "<middle_event>", "<conversion_event>"],
|
|
2898
|
+
},
|
|
2899
|
+
],
|
|
2900
|
+
range: { preset: "30d" },
|
|
2901
|
+
sort: [{ field: "label", direction: "asc" }],
|
|
2902
|
+
limit: 3,
|
|
2903
|
+
},
|
|
2904
|
+
},
|
|
2905
|
+
{
|
|
2906
|
+
id: "request_path_funnel",
|
|
2907
|
+
title: "Request path funnel",
|
|
2908
|
+
visualization: "funnel",
|
|
2909
|
+
purpose: "Measure ordered conversion through observed request paths.",
|
|
2910
|
+
usableAsIs: false,
|
|
2911
|
+
placeholders: {
|
|
2912
|
+
paths: ["/<start_path>", "/<middle_path>", "/<conversion_path>"],
|
|
2913
|
+
},
|
|
2914
|
+
adaptWith: [
|
|
2915
|
+
"Use when catalog classification includes a path-based funnel or the source code shows ordered route milestones.",
|
|
2916
|
+
"Replace placeholders with exact observed paths from the selected app.",
|
|
2917
|
+
"Do not include repeated activity routes unless source context says they are milestones.",
|
|
2918
|
+
],
|
|
2919
|
+
spec: {
|
|
2920
|
+
version: 1,
|
|
2921
|
+
dataset: "events",
|
|
2922
|
+
visualization: "funnel",
|
|
2923
|
+
metric: { name: "request_count" },
|
|
2924
|
+
groupBy: [{ field: "path" }],
|
|
2925
|
+
filters: [
|
|
2926
|
+
{
|
|
2927
|
+
field: "path",
|
|
2928
|
+
op: "in",
|
|
2929
|
+
value: ["/<start_path>", "/<middle_path>", "/<conversion_path>"],
|
|
2930
|
+
},
|
|
2931
|
+
],
|
|
2932
|
+
range: { preset: "30d" },
|
|
2933
|
+
sort: [{ field: "label", direction: "asc" }],
|
|
2934
|
+
limit: 3,
|
|
2935
|
+
},
|
|
2936
|
+
},
|
|
2937
|
+
{
|
|
2938
|
+
id: "property_breakdown",
|
|
2939
|
+
title: "Property breakdown",
|
|
2940
|
+
visualization: "bar",
|
|
2941
|
+
purpose: "Compare a promoted dimension property observed in events.",
|
|
2942
|
+
usableAsIs: false,
|
|
2943
|
+
placeholders: {
|
|
2944
|
+
property: "<promoted_dimension_property>",
|
|
2945
|
+
},
|
|
2946
|
+
adaptWith: [
|
|
2947
|
+
"Use a property key promoted by capabilities chartSpec.groupBy, such as property:plan.",
|
|
2948
|
+
"Choose stable categorical properties with enough observations to compare.",
|
|
2949
|
+
],
|
|
2950
|
+
spec: {
|
|
2951
|
+
version: 1,
|
|
2952
|
+
dataset: "events",
|
|
2953
|
+
visualization: "bar",
|
|
2954
|
+
metric: { name: "custom_event_count" },
|
|
2955
|
+
groupBy: [{ field: "property:<promoted_dimension_property>" }],
|
|
2956
|
+
filters: [],
|
|
2957
|
+
range: { preset: "30d" },
|
|
2958
|
+
sort: [{ field: "value", direction: "desc" }],
|
|
2959
|
+
limit: 10,
|
|
2960
|
+
},
|
|
2961
|
+
},
|
|
2962
|
+
{
|
|
2963
|
+
id: "average_metric_property",
|
|
2964
|
+
title: "Average metric property",
|
|
2965
|
+
visualization: "line",
|
|
2966
|
+
purpose: "Track an observed numeric event property over time.",
|
|
2967
|
+
usableAsIs: false,
|
|
2968
|
+
placeholders: {
|
|
2969
|
+
metric: "avg:<promoted_metric_property>",
|
|
2970
|
+
},
|
|
2971
|
+
adaptWith: [
|
|
2972
|
+
"Use a numeric property key promoted by capabilities chartSpec.metrics.",
|
|
2973
|
+
"Prefer averages for scores, durations, quantities, or ordinal values.",
|
|
2974
|
+
],
|
|
2975
|
+
spec: {
|
|
2976
|
+
version: 1,
|
|
2977
|
+
dataset: "events",
|
|
2978
|
+
visualization: "line",
|
|
2979
|
+
metric: { name: "avg:<promoted_metric_property>" },
|
|
2980
|
+
groupBy: [{ field: "day" }],
|
|
2981
|
+
filters: [],
|
|
2982
|
+
range: { preset: "30d" },
|
|
2983
|
+
sort: [{ field: "label", direction: "asc" }],
|
|
2984
|
+
limit: 30,
|
|
2985
|
+
},
|
|
2986
|
+
},
|
|
2987
|
+
{
|
|
2988
|
+
id: "single_number_total",
|
|
2989
|
+
title: "Single number total",
|
|
2990
|
+
visualization: "number",
|
|
2991
|
+
purpose: "Show one headline count for a selected event or request segment.",
|
|
2992
|
+
usableAsIs: false,
|
|
2993
|
+
placeholders: {
|
|
2994
|
+
eventName: "<event_name>",
|
|
2995
|
+
},
|
|
2996
|
+
adaptWith: [
|
|
2997
|
+
"Use when a dashboard needs a compact KPI card.",
|
|
2998
|
+
"Replace the event filter with an observed conversion or outcome event.",
|
|
2999
|
+
],
|
|
3000
|
+
spec: {
|
|
3001
|
+
version: 1,
|
|
3002
|
+
dataset: "events",
|
|
3003
|
+
visualization: "number",
|
|
3004
|
+
metric: { name: "custom_event_count" },
|
|
3005
|
+
groupBy: [],
|
|
3006
|
+
filters: [{ field: "event_name", op: "=", value: "<event_name>" }],
|
|
3007
|
+
range: { preset: "30d" },
|
|
3008
|
+
sort: [],
|
|
3009
|
+
limit: 1,
|
|
3010
|
+
},
|
|
3011
|
+
},
|
|
3012
|
+
];
|
|
3013
|
+
}
|
|
3014
|
+
function runAnalyticsChartsTemplates(command, io) {
|
|
3015
|
+
const templates = analyticsChartTemplates();
|
|
3016
|
+
const payload = {
|
|
3017
|
+
ok: true,
|
|
3018
|
+
command: command.command,
|
|
3019
|
+
templates,
|
|
3020
|
+
next: [
|
|
3021
|
+
"Run tender app analytics capabilities <artifact-id> --include-catalog --range 30d --json.",
|
|
3022
|
+
"Choose a template and replace placeholders with observed events, paths, or promoted property keys.",
|
|
3023
|
+
"Validate with tender app analytics query <artifact-id> --spec chart.json --json.",
|
|
3024
|
+
"Preview saved writes with tender app analytics dashboards ... --dry-run and charts create ... --dry-run.",
|
|
3025
|
+
],
|
|
3026
|
+
};
|
|
3027
|
+
if (command.json) {
|
|
3028
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
3029
|
+
}
|
|
3030
|
+
else {
|
|
3031
|
+
io.stdout([
|
|
3032
|
+
"Analytics chart templates",
|
|
3033
|
+
"",
|
|
3034
|
+
...templates.map((template) => {
|
|
3035
|
+
const marker = template.usableAsIs ? "usable as-is" : "adapt before use";
|
|
3036
|
+
return `- ${template.id}: ${template.title} (${marker})`;
|
|
3037
|
+
}),
|
|
3038
|
+
"",
|
|
3039
|
+
"Use --json to copy full chart specs.",
|
|
3040
|
+
].join("\n"));
|
|
3041
|
+
}
|
|
3042
|
+
return 0;
|
|
3043
|
+
}
|
|
2635
3044
|
function parseAnalyticsChartsCreateArgs(args) {
|
|
2636
3045
|
if (args.includes("--help") || args.includes("-h")) {
|
|
2637
3046
|
return { command: "help", topic: "app analytics charts create" };
|
|
@@ -2681,67 +3090,276 @@ function parseAnalyticsChartsCreateArgs(args) {
|
|
|
2681
3090
|
...parsed,
|
|
2682
3091
|
};
|
|
2683
3092
|
}
|
|
2684
|
-
function
|
|
2685
|
-
if (args.
|
|
2686
|
-
return { command: "help", topic: "
|
|
3093
|
+
function parseAnalyticsChartsListArgs(args) {
|
|
3094
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3095
|
+
return { command: "help", topic: "app analytics charts list" };
|
|
2687
3096
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
3097
|
+
const artifactId = args[0];
|
|
3098
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3099
|
+
throw new TenderCliUsageError("app id is required. Example: tender app analytics charts list <app-id> --dashboard <dashboard-id> --json");
|
|
2690
3100
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
if (
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
return parseAuthStatusArgs(args.slice(2));
|
|
3101
|
+
const parsed = parseAnalyticsCommonArgs(args, 1);
|
|
3102
|
+
let dashboardId = null;
|
|
3103
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3104
|
+
const arg = parsed.remaining[index];
|
|
3105
|
+
if (arg === "--dashboard") {
|
|
3106
|
+
dashboardId = parseRequiredFlagValue(parsed.remaining[index + 1], "--dashboard");
|
|
3107
|
+
index += 1;
|
|
3108
|
+
continue;
|
|
2700
3109
|
}
|
|
2701
|
-
throw new TenderCliUsageError(`Unknown
|
|
3110
|
+
throw new TenderCliUsageError(`Unknown analytics charts list option: ${arg}`);
|
|
2702
3111
|
}
|
|
2703
|
-
if (
|
|
2704
|
-
throw new TenderCliUsageError("
|
|
3112
|
+
if (!dashboardId) {
|
|
3113
|
+
throw new TenderCliUsageError("--dashboard is required. Example: tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json");
|
|
2705
3114
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
3115
|
+
return {
|
|
3116
|
+
command: "app analytics charts list",
|
|
3117
|
+
artifactId,
|
|
3118
|
+
dashboardId,
|
|
3119
|
+
...parsed,
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
function parseAnalyticsChartsUpdateArgs(args) {
|
|
3123
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3124
|
+
return { command: "help", topic: "app analytics charts update" };
|
|
2708
3125
|
}
|
|
2709
|
-
const
|
|
2710
|
-
if (
|
|
2711
|
-
|
|
2712
|
-
resourceArgs[1] === "-h") {
|
|
2713
|
-
return { command: "help", topic: "app" };
|
|
3126
|
+
const artifactId = args[0];
|
|
3127
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3128
|
+
throw new TenderCliUsageError("app id is required. Example: tender app analytics charts update <app-id> --chart <chart-id> --title <title> --spec chart.json --json");
|
|
2714
3129
|
}
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
3130
|
+
const parsed = parseAnalyticsCommonArgs(args, 1);
|
|
3131
|
+
let chartId = null;
|
|
3132
|
+
let title = null;
|
|
3133
|
+
let specPath = null;
|
|
3134
|
+
let dryRun = false;
|
|
3135
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3136
|
+
const arg = parsed.remaining[index];
|
|
3137
|
+
if (arg === "--chart") {
|
|
3138
|
+
chartId = parseRequiredFlagValue(parsed.remaining[index + 1], "--chart");
|
|
3139
|
+
index += 1;
|
|
3140
|
+
continue;
|
|
2721
3141
|
}
|
|
2722
|
-
if (
|
|
2723
|
-
|
|
3142
|
+
if (arg === "--title") {
|
|
3143
|
+
title = parseRequiredFlagValue(parsed.remaining[index + 1], "--title");
|
|
3144
|
+
index += 1;
|
|
3145
|
+
continue;
|
|
2724
3146
|
}
|
|
2725
|
-
if (
|
|
2726
|
-
|
|
3147
|
+
if (arg === "--spec") {
|
|
3148
|
+
specPath = parseSpecPath(parsed.remaining[index + 1]);
|
|
3149
|
+
index += 1;
|
|
3150
|
+
continue;
|
|
2727
3151
|
}
|
|
2728
|
-
if (
|
|
2729
|
-
|
|
3152
|
+
if (arg === "--dry-run") {
|
|
3153
|
+
dryRun = true;
|
|
3154
|
+
continue;
|
|
2730
3155
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
3156
|
+
throw new TenderCliUsageError(`Unknown analytics charts update option: ${arg}`);
|
|
3157
|
+
}
|
|
3158
|
+
if (!chartId) {
|
|
3159
|
+
throw new TenderCliUsageError("--chart is required. Example: tender app analytics charts update artifact_123 --chart analytics_chart_123 --title \"Signup funnel\" --json");
|
|
3160
|
+
}
|
|
3161
|
+
if (!title && !specPath) {
|
|
3162
|
+
throw new TenderCliUsageError("At least one of --title or --spec is required. Example: tender app analytics charts update artifact_123 --chart analytics_chart_123 --title \"Signup funnel\" --dry-run --json");
|
|
3163
|
+
}
|
|
3164
|
+
return {
|
|
3165
|
+
command: "app analytics charts update",
|
|
3166
|
+
artifactId,
|
|
3167
|
+
chartId,
|
|
3168
|
+
title,
|
|
3169
|
+
specPath,
|
|
3170
|
+
dryRun,
|
|
3171
|
+
...parsed,
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
function parseAnalyticsChartsDeleteArgs(args) {
|
|
3175
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3176
|
+
return { command: "help", topic: "app analytics charts delete" };
|
|
3177
|
+
}
|
|
3178
|
+
const artifactId = args[0];
|
|
3179
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3180
|
+
throw new TenderCliUsageError("app id is required. Example: tender app analytics charts delete <app-id> --chart <chart-id> --dry-run --json");
|
|
3181
|
+
}
|
|
3182
|
+
const parsed = parseAnalyticsCommonArgs(args, 1);
|
|
3183
|
+
let chartId = null;
|
|
3184
|
+
let dryRun = false;
|
|
3185
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3186
|
+
const arg = parsed.remaining[index];
|
|
3187
|
+
if (arg === "--chart") {
|
|
3188
|
+
chartId = parseRequiredFlagValue(parsed.remaining[index + 1], "--chart");
|
|
3189
|
+
index += 1;
|
|
3190
|
+
continue;
|
|
2733
3191
|
}
|
|
2734
|
-
if (
|
|
2735
|
-
|
|
3192
|
+
if (arg === "--dry-run") {
|
|
3193
|
+
dryRun = true;
|
|
3194
|
+
continue;
|
|
2736
3195
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
3196
|
+
throw new TenderCliUsageError(`Unknown analytics charts delete option: ${arg}`);
|
|
3197
|
+
}
|
|
3198
|
+
if (!chartId) {
|
|
3199
|
+
throw new TenderCliUsageError("--chart is required. Example: tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json");
|
|
3200
|
+
}
|
|
3201
|
+
return {
|
|
3202
|
+
command: "app analytics charts delete",
|
|
3203
|
+
artifactId,
|
|
3204
|
+
chartId,
|
|
3205
|
+
dryRun,
|
|
3206
|
+
...parsed,
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
function parseAnalyticsPropertySlotsListArgs(args) {
|
|
3210
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3211
|
+
return { command: "help", topic: "app analytics property-slots list" };
|
|
3212
|
+
}
|
|
3213
|
+
const artifactId = args[0];
|
|
3214
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3215
|
+
throw new TenderCliUsageError("app id is required. Example: tender app analytics property-slots list <app-id> --json");
|
|
3216
|
+
}
|
|
3217
|
+
const parsed = parseAnalyticsCommonArgs(args, 1);
|
|
3218
|
+
let eventName = null;
|
|
3219
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3220
|
+
const arg = parsed.remaining[index];
|
|
3221
|
+
if (arg === "--event") {
|
|
3222
|
+
eventName = parseRequiredFlagValue(parsed.remaining[index + 1], "--event");
|
|
3223
|
+
index += 1;
|
|
3224
|
+
continue;
|
|
2739
3225
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
3226
|
+
throw new TenderCliUsageError(`Unknown analytics property-slots list option: ${arg}`);
|
|
3227
|
+
}
|
|
3228
|
+
return {
|
|
3229
|
+
command: "app analytics property-slots list",
|
|
3230
|
+
artifactId,
|
|
3231
|
+
eventName,
|
|
3232
|
+
...parsed,
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
function parseAnalyticsPropertySlotsActivateArgs(args) {
|
|
3236
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3237
|
+
return { command: "help", topic: "app analytics property-slots activate" };
|
|
3238
|
+
}
|
|
3239
|
+
const artifactId = args[0];
|
|
3240
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3241
|
+
throw new TenderCliUsageError("app id is required. Example: tender app analytics property-slots activate <app-id> --event <event-name> --property <property-key> --type dimension --json");
|
|
3242
|
+
}
|
|
3243
|
+
const parsed = parseAnalyticsCommonArgs(args, 1);
|
|
3244
|
+
let eventName = null;
|
|
3245
|
+
let propertyKey = null;
|
|
3246
|
+
let slotType = null;
|
|
3247
|
+
let slotName = null;
|
|
3248
|
+
let label = null;
|
|
3249
|
+
let dryRun = false;
|
|
3250
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3251
|
+
const arg = parsed.remaining[index];
|
|
3252
|
+
if (arg === "--event") {
|
|
3253
|
+
eventName = parseRequiredFlagValue(parsed.remaining[index + 1], "--event");
|
|
3254
|
+
index += 1;
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
if (arg === "--property") {
|
|
3258
|
+
propertyKey = parseRequiredFlagValue(parsed.remaining[index + 1], "--property");
|
|
3259
|
+
index += 1;
|
|
3260
|
+
continue;
|
|
3261
|
+
}
|
|
3262
|
+
if (arg === "--type") {
|
|
3263
|
+
const value = parseRequiredFlagValue(parsed.remaining[index + 1], "--type");
|
|
3264
|
+
if (value !== "dimension" && value !== "metric") {
|
|
3265
|
+
throw new TenderCliUsageError("--type must be dimension or metric.");
|
|
3266
|
+
}
|
|
3267
|
+
slotType = value;
|
|
3268
|
+
index += 1;
|
|
3269
|
+
continue;
|
|
3270
|
+
}
|
|
3271
|
+
if (arg === "--slot") {
|
|
3272
|
+
slotName = parseRequiredFlagValue(parsed.remaining[index + 1], "--slot");
|
|
3273
|
+
index += 1;
|
|
3274
|
+
continue;
|
|
3275
|
+
}
|
|
3276
|
+
if (arg === "--label") {
|
|
3277
|
+
label = parseRequiredFlagValue(parsed.remaining[index + 1], "--label");
|
|
3278
|
+
index += 1;
|
|
3279
|
+
continue;
|
|
3280
|
+
}
|
|
3281
|
+
if (arg === "--dry-run") {
|
|
3282
|
+
dryRun = true;
|
|
3283
|
+
continue;
|
|
3284
|
+
}
|
|
3285
|
+
throw new TenderCliUsageError(`Unknown analytics property-slots activate option: ${arg}`);
|
|
3286
|
+
}
|
|
3287
|
+
if (!eventName || !propertyKey || !slotType) {
|
|
3288
|
+
throw new TenderCliUsageError("--event, --property, and --type are required. Example: tender app analytics property-slots activate artifact_123 --event signup_started --property plan --type dimension --json");
|
|
3289
|
+
}
|
|
3290
|
+
return {
|
|
3291
|
+
command: "app analytics property-slots activate",
|
|
3292
|
+
artifactId,
|
|
3293
|
+
eventName,
|
|
3294
|
+
propertyKey,
|
|
3295
|
+
slotType,
|
|
3296
|
+
slotName,
|
|
3297
|
+
label,
|
|
3298
|
+
dryRun,
|
|
3299
|
+
...parsed,
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
function parseTenderArgs(args) {
|
|
3303
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
3304
|
+
return { command: "help", topic: "root" };
|
|
3305
|
+
}
|
|
3306
|
+
if (args[0] === "capabilities") {
|
|
3307
|
+
return parseCapabilitiesArgs(args.slice(1));
|
|
3308
|
+
}
|
|
3309
|
+
if (args[0] === "auth") {
|
|
3310
|
+
if (args[1] === undefined || args[1] === "--help" || args[1] === "-h") {
|
|
3311
|
+
return { command: "help", topic: "auth" };
|
|
3312
|
+
}
|
|
3313
|
+
if (args[1] === "login") {
|
|
3314
|
+
return parseAuthLoginArgs(args.slice(2));
|
|
3315
|
+
}
|
|
3316
|
+
if (args[1] === "status") {
|
|
3317
|
+
return parseAuthStatusArgs(args.slice(2));
|
|
3318
|
+
}
|
|
3319
|
+
throw new TenderCliUsageError(`Unknown auth command: ${args[1]}`);
|
|
3320
|
+
}
|
|
3321
|
+
if (args[0] === "apps") {
|
|
3322
|
+
throw new TenderCliUsageError("Unknown command: apps. Use `tender app ...` for Tender App workflows.");
|
|
3323
|
+
}
|
|
3324
|
+
if (args[0] !== "app") {
|
|
3325
|
+
throw new TenderCliUsageError(`Unknown command: ${args[0]}`);
|
|
3326
|
+
}
|
|
3327
|
+
const resourceArgs = normalizeAppArgs(args);
|
|
3328
|
+
if (resourceArgs[1] === undefined ||
|
|
3329
|
+
resourceArgs[1] === "--help" ||
|
|
3330
|
+
resourceArgs[1] === "-h") {
|
|
3331
|
+
return { command: "help", topic: "app" };
|
|
3332
|
+
}
|
|
3333
|
+
if (resourceArgs[1] !== "doctor") {
|
|
3334
|
+
if (resourceArgs[1] === "list") {
|
|
3335
|
+
return parseArtifactsListArgs(resourceArgs.slice(2));
|
|
3336
|
+
}
|
|
3337
|
+
if (resourceArgs[1] === "create") {
|
|
3338
|
+
return parseArtifactsCreateArgs(resourceArgs.slice(2));
|
|
3339
|
+
}
|
|
3340
|
+
if (resourceArgs[1] === "update") {
|
|
3341
|
+
return parseArtifactsUpdateArgs(resourceArgs.slice(2));
|
|
3342
|
+
}
|
|
3343
|
+
if (resourceArgs[1] === "init") {
|
|
3344
|
+
return parseArtifactsInitArgs(resourceArgs.slice(2));
|
|
3345
|
+
}
|
|
3346
|
+
if (resourceArgs[1] === "context" && resourceArgs[2] === "fetch") {
|
|
3347
|
+
return parseContextFetchArgs(resourceArgs.slice(3));
|
|
3348
|
+
}
|
|
3349
|
+
if (resourceArgs[1] === "context" && resourceArgs[2] === "status") {
|
|
3350
|
+
return parseContextStatusArgs(resourceArgs.slice(3));
|
|
3351
|
+
}
|
|
3352
|
+
if (resourceArgs[1] === "context" && resourceArgs[2] === "refresh") {
|
|
3353
|
+
return parseContextRefreshArgs(resourceArgs.slice(3));
|
|
3354
|
+
}
|
|
3355
|
+
if (resourceArgs[1] === "bindings" && resourceArgs[2] === "outbound") {
|
|
3356
|
+
return parseBindingsOutboundArgs(resourceArgs.slice(3));
|
|
3357
|
+
}
|
|
3358
|
+
if (resourceArgs[1] === "bindings" &&
|
|
3359
|
+
(resourceArgs[2] === undefined ||
|
|
3360
|
+
resourceArgs[2] === "--help" ||
|
|
3361
|
+
resourceArgs[2] === "-h")) {
|
|
3362
|
+
return { command: "help", topic: "app bindings outbound" };
|
|
2745
3363
|
}
|
|
2746
3364
|
if (resourceArgs[1] === "analytics" &&
|
|
2747
3365
|
(resourceArgs[2] === undefined ||
|
|
@@ -2764,6 +3382,23 @@ function parseTenderArgs(args) {
|
|
|
2764
3382
|
if (resourceArgs[1] === "analytics" && resourceArgs[2] === "dashboards") {
|
|
2765
3383
|
return parseAnalyticsDashboardsArgs(resourceArgs.slice(3));
|
|
2766
3384
|
}
|
|
3385
|
+
if (resourceArgs[1] === "analytics" &&
|
|
3386
|
+
resourceArgs[2] === "property-slots" &&
|
|
3387
|
+
(resourceArgs[3] === undefined ||
|
|
3388
|
+
resourceArgs[3] === "--help" ||
|
|
3389
|
+
resourceArgs[3] === "-h")) {
|
|
3390
|
+
return { command: "help", topic: "app analytics property-slots list" };
|
|
3391
|
+
}
|
|
3392
|
+
if (resourceArgs[1] === "analytics" &&
|
|
3393
|
+
resourceArgs[2] === "property-slots" &&
|
|
3394
|
+
resourceArgs[3] === "list") {
|
|
3395
|
+
return parseAnalyticsPropertySlotsListArgs(resourceArgs.slice(4));
|
|
3396
|
+
}
|
|
3397
|
+
if (resourceArgs[1] === "analytics" &&
|
|
3398
|
+
resourceArgs[2] === "property-slots" &&
|
|
3399
|
+
resourceArgs[3] === "activate") {
|
|
3400
|
+
return parseAnalyticsPropertySlotsActivateArgs(resourceArgs.slice(4));
|
|
3401
|
+
}
|
|
2767
3402
|
if (resourceArgs[1] === "analytics" &&
|
|
2768
3403
|
resourceArgs[2] === "export" &&
|
|
2769
3404
|
(resourceArgs[3] === undefined ||
|
|
@@ -2781,11 +3416,38 @@ function parseTenderArgs(args) {
|
|
|
2781
3416
|
resourceArgs[3] === "query") {
|
|
2782
3417
|
return parseAnalyticsExportQueryArgs(resourceArgs.slice(4));
|
|
2783
3418
|
}
|
|
3419
|
+
if (resourceArgs[1] === "analytics" &&
|
|
3420
|
+
resourceArgs[2] === "charts" &&
|
|
3421
|
+
resourceArgs[3] === "templates") {
|
|
3422
|
+
return parseAnalyticsChartsTemplatesArgs(resourceArgs.slice(4));
|
|
3423
|
+
}
|
|
3424
|
+
if (resourceArgs[1] === "analytics" &&
|
|
3425
|
+
resourceArgs[2] === "charts" &&
|
|
3426
|
+
(resourceArgs[3] === undefined ||
|
|
3427
|
+
resourceArgs[3] === "--help" ||
|
|
3428
|
+
resourceArgs[3] === "-h")) {
|
|
3429
|
+
return { command: "help", topic: "app analytics charts templates" };
|
|
3430
|
+
}
|
|
2784
3431
|
if (resourceArgs[1] === "analytics" &&
|
|
2785
3432
|
resourceArgs[2] === "charts" &&
|
|
2786
3433
|
resourceArgs[3] === "create") {
|
|
2787
3434
|
return parseAnalyticsChartsCreateArgs(resourceArgs.slice(4));
|
|
2788
3435
|
}
|
|
3436
|
+
if (resourceArgs[1] === "analytics" &&
|
|
3437
|
+
resourceArgs[2] === "charts" &&
|
|
3438
|
+
resourceArgs[3] === "list") {
|
|
3439
|
+
return parseAnalyticsChartsListArgs(resourceArgs.slice(4));
|
|
3440
|
+
}
|
|
3441
|
+
if (resourceArgs[1] === "analytics" &&
|
|
3442
|
+
resourceArgs[2] === "charts" &&
|
|
3443
|
+
resourceArgs[3] === "update") {
|
|
3444
|
+
return parseAnalyticsChartsUpdateArgs(resourceArgs.slice(4));
|
|
3445
|
+
}
|
|
3446
|
+
if (resourceArgs[1] === "analytics" &&
|
|
3447
|
+
resourceArgs[2] === "charts" &&
|
|
3448
|
+
resourceArgs[3] === "delete") {
|
|
3449
|
+
return parseAnalyticsChartsDeleteArgs(resourceArgs.slice(4));
|
|
3450
|
+
}
|
|
2789
3451
|
if (resourceArgs[1] === "context" &&
|
|
2790
3452
|
(resourceArgs[2] === undefined ||
|
|
2791
3453
|
resourceArgs[2] === "--help" ||
|
|
@@ -3158,7 +3820,9 @@ async function requestAnalyticsApi(input) {
|
|
|
3158
3820
|
if (input.includeCatalog) {
|
|
3159
3821
|
params.set("includeCatalog", "1");
|
|
3160
3822
|
}
|
|
3161
|
-
const suffix = input.analyticsPath
|
|
3823
|
+
const suffix = input.analyticsPath
|
|
3824
|
+
? `/${input.analyticsPath}${input.resourceId ? `/${encodeURIComponent(input.resourceId)}` : ""}`
|
|
3825
|
+
: "";
|
|
3162
3826
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
3163
3827
|
const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/analytics${suffix}${query}`, {
|
|
3164
3828
|
method: input.method,
|
|
@@ -3176,12 +3840,12 @@ async function requestAnalyticsApi(input) {
|
|
|
3176
3840
|
const message = (typeof payload?.message === "string" && payload.message) ||
|
|
3177
3841
|
(typeof payload?.reasonCode === "string" && payload.reasonCode) ||
|
|
3178
3842
|
`Analytics request failed with HTTP ${response.status}.`;
|
|
3179
|
-
throw new
|
|
3843
|
+
throw new TenderCliAnalyticsApiError([
|
|
3180
3844
|
message,
|
|
3181
3845
|
`Scope: artifact ${input.artifactId}, analytics/${input.analyticsPath || "summary"}.`,
|
|
3182
3846
|
`Reason: ${reasonCode}.`,
|
|
3183
3847
|
`Example: ${analyticsCorrectedExample(input.artifactId, input.analyticsPath)}.`,
|
|
3184
|
-
].join("\n"));
|
|
3848
|
+
].join("\n"), reasonCode);
|
|
3185
3849
|
}
|
|
3186
3850
|
return payload;
|
|
3187
3851
|
}
|
|
@@ -3195,6 +3859,9 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
|
|
|
3195
3859
|
if (analyticsPath === "suggestions") {
|
|
3196
3860
|
return `tender app analytics suggestions ${artifactId} --range 30d --json`;
|
|
3197
3861
|
}
|
|
3862
|
+
if (analyticsPath === "property-slots") {
|
|
3863
|
+
return `tender app analytics property-slots list ${artifactId} --json`;
|
|
3864
|
+
}
|
|
3198
3865
|
if (analyticsPath === "dashboards") {
|
|
3199
3866
|
return `tender app analytics dashboards ${artifactId} --json`;
|
|
3200
3867
|
}
|
|
@@ -3203,6 +3870,332 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
|
|
|
3203
3870
|
}
|
|
3204
3871
|
return `tender app analytics summary ${artifactId} --range 30d --json`;
|
|
3205
3872
|
}
|
|
3873
|
+
function isCliJsonRecord(value) {
|
|
3874
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3875
|
+
}
|
|
3876
|
+
function readCliStringField(record, field) {
|
|
3877
|
+
const value = record[field];
|
|
3878
|
+
return typeof value === "string" ? value : null;
|
|
3879
|
+
}
|
|
3880
|
+
function readAnalyticsDashboardRecords(payload) {
|
|
3881
|
+
return Array.isArray(payload.dashboards)
|
|
3882
|
+
? payload.dashboards.filter(isCliJsonRecord)
|
|
3883
|
+
: [];
|
|
3884
|
+
}
|
|
3885
|
+
function readAnalyticsChartRecords(dashboard) {
|
|
3886
|
+
return Array.isArray(dashboard.charts)
|
|
3887
|
+
? dashboard.charts.filter(isCliJsonRecord)
|
|
3888
|
+
: [];
|
|
3889
|
+
}
|
|
3890
|
+
function findAnalyticsDashboard(payload, dashboardId) {
|
|
3891
|
+
return (readAnalyticsDashboardRecords(payload).find((dashboard) => readCliStringField(dashboard, "id") === dashboardId) ?? null);
|
|
3892
|
+
}
|
|
3893
|
+
function findAnalyticsChart(payload, chartId) {
|
|
3894
|
+
for (const dashboard of readAnalyticsDashboardRecords(payload)) {
|
|
3895
|
+
const chart = readAnalyticsChartRecords(dashboard).find((candidate) => readCliStringField(candidate, "id") === chartId);
|
|
3896
|
+
if (chart) {
|
|
3897
|
+
return { dashboard, chart };
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
return null;
|
|
3901
|
+
}
|
|
3902
|
+
function readAnalyticsChartTitle(chart) {
|
|
3903
|
+
return readCliStringField(chart, "title") ?? "Analytics chart";
|
|
3904
|
+
}
|
|
3905
|
+
function readAnalyticsChartSpec(chart) {
|
|
3906
|
+
const spec = chart.spec;
|
|
3907
|
+
if (spec === undefined) {
|
|
3908
|
+
throw new Error("Saved chart does not include a chart spec.");
|
|
3909
|
+
}
|
|
3910
|
+
return spec;
|
|
3911
|
+
}
|
|
3912
|
+
function readAnalyticsChartLayout(chart) {
|
|
3913
|
+
return isCliJsonRecord(chart.layout) ? chart.layout : {};
|
|
3914
|
+
}
|
|
3915
|
+
function readAnalyticsPropertySlotRecords(payload) {
|
|
3916
|
+
return Array.isArray(payload.propertySlots)
|
|
3917
|
+
? payload.propertySlots.filter(isCliJsonRecord)
|
|
3918
|
+
: [];
|
|
3919
|
+
}
|
|
3920
|
+
function readAnalyticsSpecEventNameFilter(spec) {
|
|
3921
|
+
if (!isCliJsonRecord(spec) || !Array.isArray(spec.filters)) {
|
|
3922
|
+
return null;
|
|
3923
|
+
}
|
|
3924
|
+
const filter = spec.filters.filter(isCliJsonRecord).find((candidate) => readCliStringField(candidate, "field") === "event_name" &&
|
|
3925
|
+
readCliStringField(candidate, "op") === "=" &&
|
|
3926
|
+
typeof candidate.value === "string" &&
|
|
3927
|
+
candidate.value.trim().length > 0);
|
|
3928
|
+
return typeof filter?.value === "string" ? filter.value.trim() : null;
|
|
3929
|
+
}
|
|
3930
|
+
function readAnalyticsSpecMetricName(spec) {
|
|
3931
|
+
if (!isCliJsonRecord(spec) || !isCliJsonRecord(spec.metric)) {
|
|
3932
|
+
return null;
|
|
3933
|
+
}
|
|
3934
|
+
return readCliStringField(spec.metric, "name");
|
|
3935
|
+
}
|
|
3936
|
+
function readAnalyticsSpecGroupField(spec) {
|
|
3937
|
+
if (!isCliJsonRecord(spec) || !Array.isArray(spec.groupBy)) {
|
|
3938
|
+
return null;
|
|
3939
|
+
}
|
|
3940
|
+
const [group] = spec.groupBy.filter(isCliJsonRecord);
|
|
3941
|
+
return group ? readCliStringField(group, "field") : null;
|
|
3942
|
+
}
|
|
3943
|
+
function readRequiredAnalyticsPropertySlotsFromSpec(spec) {
|
|
3944
|
+
const eventName = readAnalyticsSpecEventNameFilter(spec);
|
|
3945
|
+
const requirements = [];
|
|
3946
|
+
const groupField = readAnalyticsSpecGroupField(spec);
|
|
3947
|
+
if (groupField?.startsWith("property:")) {
|
|
3948
|
+
if (!eventName) {
|
|
3949
|
+
throw new TenderCliUsageError("Property group-by chart specs require an event_name = filter so the CLI can activate the promoted property slot.");
|
|
3950
|
+
}
|
|
3951
|
+
requirements.push({
|
|
3952
|
+
eventName,
|
|
3953
|
+
propertyKey: groupField.slice("property:".length),
|
|
3954
|
+
slotType: "dimension",
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3957
|
+
const metricName = readAnalyticsSpecMetricName(spec);
|
|
3958
|
+
if (metricName?.startsWith("sum:") || metricName?.startsWith("avg:")) {
|
|
3959
|
+
if (!eventName) {
|
|
3960
|
+
throw new TenderCliUsageError("Property metric chart specs require an event_name = filter so the CLI can activate the promoted property slot.");
|
|
3961
|
+
}
|
|
3962
|
+
requirements.push({
|
|
3963
|
+
eventName,
|
|
3964
|
+
propertyKey: metricName.slice(metricName.indexOf(":") + 1),
|
|
3965
|
+
slotType: "metric",
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
const seen = new Set();
|
|
3969
|
+
return requirements.filter((requirement) => {
|
|
3970
|
+
const key = `${requirement.slotType}:${requirement.eventName}:${requirement.propertyKey}`;
|
|
3971
|
+
if (seen.has(key)) {
|
|
3972
|
+
return false;
|
|
3973
|
+
}
|
|
3974
|
+
seen.add(key);
|
|
3975
|
+
return true;
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
function analyticsPropertySlotNames(slotType) {
|
|
3979
|
+
return slotType === "dimension"
|
|
3980
|
+
? Array.from({ length: 13 }, (_, index) => `blob${index + 8}`)
|
|
3981
|
+
: Array.from({ length: 19 }, (_, index) => `double${index + 2}`);
|
|
3982
|
+
}
|
|
3983
|
+
function isValidAnalyticsPropertySlotName(input) {
|
|
3984
|
+
return analyticsPropertySlotNames(input.slotType).includes(input.slotName);
|
|
3985
|
+
}
|
|
3986
|
+
function findExistingAnalyticsPropertySlot(input) {
|
|
3987
|
+
return (input.propertySlots.find((slot) => readCliStringField(slot, "status") === "active" &&
|
|
3988
|
+
readCliStringField(slot, "eventName") === input.requirement.eventName &&
|
|
3989
|
+
readCliStringField(slot, "propertyKey") === input.requirement.propertyKey &&
|
|
3990
|
+
readCliStringField(slot, "slotType") === input.requirement.slotType) ?? null);
|
|
3991
|
+
}
|
|
3992
|
+
function chooseAnalyticsPropertySlotName(input) {
|
|
3993
|
+
if (input.requestedSlotName) {
|
|
3994
|
+
if (!isValidAnalyticsPropertySlotName({
|
|
3995
|
+
slotType: input.requirement.slotType,
|
|
3996
|
+
slotName: input.requestedSlotName,
|
|
3997
|
+
})) {
|
|
3998
|
+
throw new TenderCliUsageError(`--slot must be a valid ${input.requirement.slotType} analytics slot.`);
|
|
3999
|
+
}
|
|
4000
|
+
const conflictingSlot = input.propertySlots.find((slot) => readCliStringField(slot, "status") === "active" &&
|
|
4001
|
+
readCliStringField(slot, "eventName") === input.requirement.eventName &&
|
|
4002
|
+
readCliStringField(slot, "slotType") === input.requirement.slotType &&
|
|
4003
|
+
readCliStringField(slot, "slotName") === input.requestedSlotName &&
|
|
4004
|
+
readCliStringField(slot, "propertyKey") !== input.requirement.propertyKey);
|
|
4005
|
+
if (conflictingSlot) {
|
|
4006
|
+
throw new TenderCliUsageError(`--slot ${input.requestedSlotName} is already active for another ${input.requirement.slotType} property on ${input.requirement.eventName}.`);
|
|
4007
|
+
}
|
|
4008
|
+
return input.requestedSlotName;
|
|
4009
|
+
}
|
|
4010
|
+
const usedSlots = new Set(input.propertySlots
|
|
4011
|
+
.filter((slot) => readCliStringField(slot, "status") === "active" &&
|
|
4012
|
+
readCliStringField(slot, "eventName") === input.requirement.eventName &&
|
|
4013
|
+
readCliStringField(slot, "slotType") === input.requirement.slotType)
|
|
4014
|
+
.map((slot) => readCliStringField(slot, "slotName"))
|
|
4015
|
+
.filter((slotName) => Boolean(slotName)));
|
|
4016
|
+
const slotName = analyticsPropertySlotNames(input.requirement.slotType).find((candidate) => !usedSlots.has(candidate));
|
|
4017
|
+
if (!slotName) {
|
|
4018
|
+
throw new Error("analytics_property_slots_full");
|
|
4019
|
+
}
|
|
4020
|
+
return slotName;
|
|
4021
|
+
}
|
|
4022
|
+
async function fetchAnalyticsPropertySlots(input) {
|
|
4023
|
+
const payload = await requestAnalyticsApi({
|
|
4024
|
+
artifactId: input.command.artifactId,
|
|
4025
|
+
analyticsPath: "property-slots",
|
|
4026
|
+
method: "GET",
|
|
4027
|
+
baseUrl: input.credentials.baseUrl,
|
|
4028
|
+
token: input.credentials.token,
|
|
4029
|
+
fetcher: input.runtime.fetcher ?? fetch,
|
|
4030
|
+
});
|
|
4031
|
+
return { payload, propertySlots: readAnalyticsPropertySlotRecords(payload) };
|
|
4032
|
+
}
|
|
4033
|
+
async function ensureAnalyticsPropertySlots(input) {
|
|
4034
|
+
if (input.requirements.length === 0) {
|
|
4035
|
+
return [];
|
|
4036
|
+
}
|
|
4037
|
+
const { propertySlots } = await fetchAnalyticsPropertySlots({
|
|
4038
|
+
command: input.command,
|
|
4039
|
+
credentials: input.credentials,
|
|
4040
|
+
runtime: input.runtime,
|
|
4041
|
+
});
|
|
4042
|
+
const plan = [];
|
|
4043
|
+
for (const requirement of input.requirements) {
|
|
4044
|
+
const existing = findExistingAnalyticsPropertySlot({
|
|
4045
|
+
propertySlots,
|
|
4046
|
+
requirement,
|
|
4047
|
+
});
|
|
4048
|
+
if (existing) {
|
|
4049
|
+
plan.push({
|
|
4050
|
+
...requirement,
|
|
4051
|
+
slotName: readCliStringField(existing, "slotName") ?? "",
|
|
4052
|
+
label: readCliStringField(existing, "label") ?? requirement.propertyKey,
|
|
4053
|
+
status: "existing",
|
|
4054
|
+
propertySlot: existing,
|
|
4055
|
+
});
|
|
4056
|
+
continue;
|
|
4057
|
+
}
|
|
4058
|
+
const slotName = chooseAnalyticsPropertySlotName({
|
|
4059
|
+
propertySlots,
|
|
4060
|
+
requirement,
|
|
4061
|
+
});
|
|
4062
|
+
const body = {
|
|
4063
|
+
eventName: requirement.eventName,
|
|
4064
|
+
propertyKey: requirement.propertyKey,
|
|
4065
|
+
slotType: requirement.slotType,
|
|
4066
|
+
slotName,
|
|
4067
|
+
label: requirement.propertyKey,
|
|
4068
|
+
};
|
|
4069
|
+
if (input.dryRun) {
|
|
4070
|
+
plan.push({
|
|
4071
|
+
...body,
|
|
4072
|
+
status: "planned",
|
|
4073
|
+
});
|
|
4074
|
+
continue;
|
|
4075
|
+
}
|
|
4076
|
+
const payload = await requestAnalyticsApi({
|
|
4077
|
+
artifactId: input.command.artifactId,
|
|
4078
|
+
analyticsPath: "property-slots",
|
|
4079
|
+
method: "POST",
|
|
4080
|
+
baseUrl: input.credentials.baseUrl,
|
|
4081
|
+
token: input.credentials.token,
|
|
4082
|
+
fetcher: input.runtime.fetcher ?? fetch,
|
|
4083
|
+
body,
|
|
4084
|
+
});
|
|
4085
|
+
plan.push({
|
|
4086
|
+
...body,
|
|
4087
|
+
status: "activated",
|
|
4088
|
+
propertySlot: payload.propertySlot,
|
|
4089
|
+
});
|
|
4090
|
+
propertySlots.push({
|
|
4091
|
+
eventName: body.eventName,
|
|
4092
|
+
propertyKey: body.propertyKey,
|
|
4093
|
+
slotType: body.slotType,
|
|
4094
|
+
slotName: body.slotName,
|
|
4095
|
+
label: body.label,
|
|
4096
|
+
status: "active",
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
return plan;
|
|
4100
|
+
}
|
|
4101
|
+
function analyticsCommandAuthFlags(command) {
|
|
4102
|
+
const flags = [];
|
|
4103
|
+
if (command.baseUrl) {
|
|
4104
|
+
flags.push("--base-url", shellQuote(command.baseUrl));
|
|
4105
|
+
}
|
|
4106
|
+
if (command.profileName) {
|
|
4107
|
+
flags.push("--profile", shellQuote(command.profileName));
|
|
4108
|
+
}
|
|
4109
|
+
return flags;
|
|
4110
|
+
}
|
|
4111
|
+
function analyticsPropertySlotActivationCommand(input) {
|
|
4112
|
+
return [
|
|
4113
|
+
"tender",
|
|
4114
|
+
"app",
|
|
4115
|
+
"analytics",
|
|
4116
|
+
"property-slots",
|
|
4117
|
+
"activate",
|
|
4118
|
+
shellQuote(input.command.artifactId),
|
|
4119
|
+
"--event",
|
|
4120
|
+
shellQuote(input.requirement.eventName),
|
|
4121
|
+
"--property",
|
|
4122
|
+
shellQuote(input.requirement.propertyKey),
|
|
4123
|
+
"--type",
|
|
4124
|
+
input.requirement.slotType,
|
|
4125
|
+
...(input.dryRun ? ["--dry-run"] : []),
|
|
4126
|
+
...analyticsCommandAuthFlags(input.command),
|
|
4127
|
+
"--json",
|
|
4128
|
+
].join(" ");
|
|
4129
|
+
}
|
|
4130
|
+
function createAnalyticsPropertySlotGuidance(input) {
|
|
4131
|
+
let requirements;
|
|
4132
|
+
try {
|
|
4133
|
+
requirements = readRequiredAnalyticsPropertySlotsFromSpec(input.spec);
|
|
4134
|
+
}
|
|
4135
|
+
catch {
|
|
4136
|
+
return null;
|
|
4137
|
+
}
|
|
4138
|
+
if (requirements.length === 0) {
|
|
4139
|
+
return null;
|
|
4140
|
+
}
|
|
4141
|
+
const lines = [
|
|
4142
|
+
"To make this chart available for dashboard queries, activate the custom event property first:",
|
|
4143
|
+
];
|
|
4144
|
+
for (const requirement of requirements) {
|
|
4145
|
+
lines.push(analyticsPropertySlotActivationCommand({
|
|
4146
|
+
command: input.command,
|
|
4147
|
+
requirement,
|
|
4148
|
+
dryRun: true,
|
|
4149
|
+
}), analyticsPropertySlotActivationCommand({
|
|
4150
|
+
command: input.command,
|
|
4151
|
+
requirement,
|
|
4152
|
+
dryRun: false,
|
|
4153
|
+
}));
|
|
4154
|
+
}
|
|
4155
|
+
if (input.command.flagToken) {
|
|
4156
|
+
lines.push("Pass the same --token again, or use a saved profile/TENDER_API_TOKEN; tokens are not echoed in suggestions.");
|
|
4157
|
+
}
|
|
4158
|
+
lines.push("Then rerun the original analytics query.");
|
|
4159
|
+
return lines.join("\n");
|
|
4160
|
+
}
|
|
4161
|
+
async function requestAnalyticsQueryWithPropertyGuidance(input) {
|
|
4162
|
+
try {
|
|
4163
|
+
return await requestAnalyticsApi({
|
|
4164
|
+
artifactId: input.command.artifactId,
|
|
4165
|
+
analyticsPath: "query",
|
|
4166
|
+
method: "POST",
|
|
4167
|
+
baseUrl: input.credentials.baseUrl,
|
|
4168
|
+
token: input.credentials.token,
|
|
4169
|
+
fetcher: input.runtime.fetcher ?? fetch,
|
|
4170
|
+
body: { spec: input.spec },
|
|
4171
|
+
});
|
|
4172
|
+
}
|
|
4173
|
+
catch (error) {
|
|
4174
|
+
if (error instanceof TenderCliAnalyticsApiError &&
|
|
4175
|
+
error.reasonCode === "analytics_property_slot_not_found") {
|
|
4176
|
+
const guidance = createAnalyticsPropertySlotGuidance({
|
|
4177
|
+
command: input.command,
|
|
4178
|
+
spec: input.spec,
|
|
4179
|
+
});
|
|
4180
|
+
if (guidance) {
|
|
4181
|
+
throw new TenderCliAnalyticsApiError(`${error.message}\n\n${guidance}`, error.reasonCode);
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
throw error;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
async function requestAnalyticsDashboardsForCli(command, runtime) {
|
|
4188
|
+
const credentials = await resolveAnalyticsCredentials(command, runtime);
|
|
4189
|
+
const payload = await requestAnalyticsApi({
|
|
4190
|
+
artifactId: command.artifactId,
|
|
4191
|
+
analyticsPath: "dashboards",
|
|
4192
|
+
method: "GET",
|
|
4193
|
+
baseUrl: credentials.baseUrl,
|
|
4194
|
+
token: credentials.token,
|
|
4195
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
4196
|
+
});
|
|
4197
|
+
return { credentials, payload };
|
|
4198
|
+
}
|
|
3206
4199
|
function printAnalyticsPayload(command, io, payload, fallbackLines) {
|
|
3207
4200
|
if (command.json) {
|
|
3208
4201
|
io.stdout(JSON.stringify(payload, null, 2));
|
|
@@ -3491,6 +4484,47 @@ async function planSourceFiles(input) {
|
|
|
3491
4484
|
}
|
|
3492
4485
|
async function runAuthStatus(command, io, runtime) {
|
|
3493
4486
|
const env = runtime.env ?? process.env;
|
|
4487
|
+
if (command.profileName) {
|
|
4488
|
+
const config = await readConfig(env);
|
|
4489
|
+
const profile = config.profiles?.[command.profileName] ?? null;
|
|
4490
|
+
const result = {
|
|
4491
|
+
ok: Boolean(profile),
|
|
4492
|
+
command: command.command,
|
|
4493
|
+
source: profile ? "config" : null,
|
|
4494
|
+
profile: command.profileName,
|
|
4495
|
+
configPath: readConfigPath(env),
|
|
4496
|
+
baseUrl: profile?.baseUrl ?? null,
|
|
4497
|
+
tokenAvailable: Boolean(profile?.token),
|
|
4498
|
+
tokenPrefix: profile?.tokenPrefix ?? null,
|
|
4499
|
+
tenantId: profile?.tenantId ?? null,
|
|
4500
|
+
artifactId: profile?.artifactId ?? null,
|
|
4501
|
+
permissions: profile?.permissions ?? [],
|
|
4502
|
+
expiresAt: formatExpiry(profile?.expiresAt),
|
|
4503
|
+
};
|
|
4504
|
+
if (command.json) {
|
|
4505
|
+
io.stdout(JSON.stringify(result, null, 2));
|
|
4506
|
+
}
|
|
4507
|
+
else if (profile) {
|
|
4508
|
+
io.stdout([
|
|
4509
|
+
"authenticated: true",
|
|
4510
|
+
`source: config:${command.profileName}`,
|
|
4511
|
+
`base_url: ${profile.baseUrl}`,
|
|
4512
|
+
`token_prefix: ${profile.tokenPrefix ?? "(unknown)"}`,
|
|
4513
|
+
`tenant_id: ${profile.tenantId ?? "(unknown)"}`,
|
|
4514
|
+
`app_id: ${profile.artifactId ?? "(account)"}`,
|
|
4515
|
+
`permissions: ${(profile.permissions ?? []).join(" ") || "(unknown)"}`,
|
|
4516
|
+
`expires_at: ${formatExpiry(profile.expiresAt) ?? "(unknown)"}`,
|
|
4517
|
+
].join("\n"));
|
|
4518
|
+
}
|
|
4519
|
+
else {
|
|
4520
|
+
io.stdout([
|
|
4521
|
+
"authenticated: false",
|
|
4522
|
+
`profile: ${command.profileName}`,
|
|
4523
|
+
"next: tender auth login --device --profile edit-preview --artifact <artifact-id>",
|
|
4524
|
+
].join("\n"));
|
|
4525
|
+
}
|
|
4526
|
+
return profile ? 0 : 1;
|
|
4527
|
+
}
|
|
3494
4528
|
if (env.TENDER_API_TOKEN) {
|
|
3495
4529
|
const result = {
|
|
3496
4530
|
ok: true,
|
|
@@ -5207,14 +6241,11 @@ async function runAnalyticsQuery(command, io, runtime) {
|
|
|
5207
6241
|
specPath: command.specPath,
|
|
5208
6242
|
stdin: runtime.stdin,
|
|
5209
6243
|
});
|
|
5210
|
-
const payload = await
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
token: credentials.token,
|
|
5216
|
-
fetcher: runtime.fetcher ?? fetch,
|
|
5217
|
-
body: { spec },
|
|
6244
|
+
const payload = await requestAnalyticsQueryWithPropertyGuidance({
|
|
6245
|
+
command,
|
|
6246
|
+
credentials,
|
|
6247
|
+
runtime,
|
|
6248
|
+
spec,
|
|
5218
6249
|
});
|
|
5219
6250
|
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
5220
6251
|
printAnalyticsPayload(command, io, payload, [
|
|
@@ -5335,21 +6366,119 @@ async function runAnalyticsExportQuery(command, io, runtime) {
|
|
|
5335
6366
|
specPath: command.specPath,
|
|
5336
6367
|
stdin: runtime.stdin,
|
|
5337
6368
|
});
|
|
6369
|
+
const payload = await requestAnalyticsQueryWithPropertyGuidance({
|
|
6370
|
+
command,
|
|
6371
|
+
credentials,
|
|
6372
|
+
runtime,
|
|
6373
|
+
spec,
|
|
6374
|
+
});
|
|
6375
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
6376
|
+
if (command.format === "csv") {
|
|
6377
|
+
io.stdout(rowsToCsv(rows));
|
|
6378
|
+
return 0;
|
|
6379
|
+
}
|
|
6380
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
6381
|
+
return 0;
|
|
6382
|
+
}
|
|
6383
|
+
async function runAnalyticsPropertySlotsList(command, io, runtime) {
|
|
6384
|
+
const credentials = await resolveAnalyticsCredentials(command, runtime);
|
|
5338
6385
|
const payload = await requestAnalyticsApi({
|
|
5339
6386
|
artifactId: command.artifactId,
|
|
5340
|
-
analyticsPath: "
|
|
5341
|
-
method: "
|
|
6387
|
+
analyticsPath: "property-slots",
|
|
6388
|
+
method: "GET",
|
|
5342
6389
|
baseUrl: credentials.baseUrl,
|
|
5343
6390
|
token: credentials.token,
|
|
5344
6391
|
fetcher: runtime.fetcher ?? fetch,
|
|
5345
|
-
body: { spec },
|
|
5346
6392
|
});
|
|
5347
|
-
const
|
|
5348
|
-
|
|
5349
|
-
|
|
6393
|
+
const propertySlots = readAnalyticsPropertySlotRecords(payload).filter((slot) => !command.eventName ||
|
|
6394
|
+
readCliStringField(slot, "eventName") === command.eventName);
|
|
6395
|
+
printAnalyticsPayload(command, io, {
|
|
6396
|
+
ok: true,
|
|
6397
|
+
command: command.command,
|
|
6398
|
+
artifactId: command.artifactId,
|
|
6399
|
+
eventName: command.eventName,
|
|
6400
|
+
propertySlots,
|
|
6401
|
+
}, [
|
|
6402
|
+
`app_id: ${command.artifactId}`,
|
|
6403
|
+
command.eventName ? `event: ${command.eventName}` : "event: all",
|
|
6404
|
+
`property_slots: ${propertySlots.length}`,
|
|
6405
|
+
]);
|
|
6406
|
+
return 0;
|
|
6407
|
+
}
|
|
6408
|
+
async function runAnalyticsPropertySlotsActivate(command, io, runtime) {
|
|
6409
|
+
const credentials = await resolveAnalyticsCredentials(command, runtime);
|
|
6410
|
+
const requirement = {
|
|
6411
|
+
eventName: command.eventName,
|
|
6412
|
+
propertyKey: command.propertyKey,
|
|
6413
|
+
slotType: command.slotType,
|
|
6414
|
+
};
|
|
6415
|
+
const { propertySlots } = await fetchAnalyticsPropertySlots({
|
|
6416
|
+
command,
|
|
6417
|
+
credentials,
|
|
6418
|
+
runtime,
|
|
6419
|
+
});
|
|
6420
|
+
const existing = findExistingAnalyticsPropertySlot({
|
|
6421
|
+
propertySlots,
|
|
6422
|
+
requirement,
|
|
6423
|
+
});
|
|
6424
|
+
if (existing) {
|
|
6425
|
+
const payload = {
|
|
6426
|
+
ok: true,
|
|
6427
|
+
command: command.command,
|
|
6428
|
+
artifactId: command.artifactId,
|
|
6429
|
+
alreadyActive: true,
|
|
6430
|
+
propertySlot: existing,
|
|
6431
|
+
};
|
|
6432
|
+
printAnalyticsPayload(command, io, payload, [
|
|
6433
|
+
`app_id: ${command.artifactId}`,
|
|
6434
|
+
"already_active: true",
|
|
6435
|
+
`slot_name: ${readCliStringField(existing, "slotName") ?? ""}`,
|
|
6436
|
+
]);
|
|
5350
6437
|
return 0;
|
|
5351
6438
|
}
|
|
5352
|
-
|
|
6439
|
+
const slotName = chooseAnalyticsPropertySlotName({
|
|
6440
|
+
propertySlots,
|
|
6441
|
+
requirement,
|
|
6442
|
+
requestedSlotName: command.slotName,
|
|
6443
|
+
});
|
|
6444
|
+
const request = {
|
|
6445
|
+
eventName: command.eventName,
|
|
6446
|
+
propertyKey: command.propertyKey,
|
|
6447
|
+
slotType: command.slotType,
|
|
6448
|
+
slotName,
|
|
6449
|
+
label: command.label ?? command.propertyKey,
|
|
6450
|
+
};
|
|
6451
|
+
if (command.dryRun) {
|
|
6452
|
+
printAnalyticsPayload(command, io, {
|
|
6453
|
+
ok: true,
|
|
6454
|
+
command: command.command,
|
|
6455
|
+
dryRun: true,
|
|
6456
|
+
artifactId: command.artifactId,
|
|
6457
|
+
request,
|
|
6458
|
+
}, [
|
|
6459
|
+
`app_id: ${command.artifactId}`,
|
|
6460
|
+
`event: ${command.eventName}`,
|
|
6461
|
+
`property: ${command.propertyKey}`,
|
|
6462
|
+
`slot_name: ${slotName}`,
|
|
6463
|
+
"dry_run: true",
|
|
6464
|
+
]);
|
|
6465
|
+
return 0;
|
|
6466
|
+
}
|
|
6467
|
+
const payload = await requestAnalyticsApi({
|
|
6468
|
+
artifactId: command.artifactId,
|
|
6469
|
+
analyticsPath: "property-slots",
|
|
6470
|
+
method: "POST",
|
|
6471
|
+
baseUrl: credentials.baseUrl,
|
|
6472
|
+
token: credentials.token,
|
|
6473
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6474
|
+
body: request,
|
|
6475
|
+
});
|
|
6476
|
+
printAnalyticsPayload(command, io, payload, [
|
|
6477
|
+
`app_id: ${command.artifactId}`,
|
|
6478
|
+
`event: ${command.eventName}`,
|
|
6479
|
+
`property: ${command.propertyKey}`,
|
|
6480
|
+
`slot_name: ${slotName}`,
|
|
6481
|
+
]);
|
|
5353
6482
|
return 0;
|
|
5354
6483
|
}
|
|
5355
6484
|
async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
@@ -5363,11 +6492,25 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
5363
6492
|
spec,
|
|
5364
6493
|
layout: {},
|
|
5365
6494
|
};
|
|
6495
|
+
const requiredPropertySlots = readRequiredAnalyticsPropertySlotsFromSpec(spec);
|
|
6496
|
+
const credentials = !command.dryRun || requiredPropertySlots.length > 0
|
|
6497
|
+
? await resolveAnalyticsCredentials(command, runtime)
|
|
6498
|
+
: null;
|
|
6499
|
+
const propertySlots = credentials
|
|
6500
|
+
? await ensureAnalyticsPropertySlots({
|
|
6501
|
+
command,
|
|
6502
|
+
credentials,
|
|
6503
|
+
runtime,
|
|
6504
|
+
requirements: requiredPropertySlots,
|
|
6505
|
+
dryRun: command.dryRun,
|
|
6506
|
+
})
|
|
6507
|
+
: [];
|
|
5366
6508
|
if (command.dryRun) {
|
|
5367
6509
|
const payload = {
|
|
5368
6510
|
ok: true,
|
|
5369
6511
|
dryRun: true,
|
|
5370
6512
|
artifactId: command.artifactId,
|
|
6513
|
+
propertySlots,
|
|
5371
6514
|
request: body,
|
|
5372
6515
|
};
|
|
5373
6516
|
printAnalyticsPayload(command, io, payload, [
|
|
@@ -5378,7 +6521,9 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
5378
6521
|
]);
|
|
5379
6522
|
return 0;
|
|
5380
6523
|
}
|
|
5381
|
-
|
|
6524
|
+
if (!credentials) {
|
|
6525
|
+
throw new Error("Analytics credentials are required to create saved charts.");
|
|
6526
|
+
}
|
|
5382
6527
|
const payload = await requestAnalyticsApi({
|
|
5383
6528
|
artifactId: command.artifactId,
|
|
5384
6529
|
analyticsPath: "charts",
|
|
@@ -5388,9 +6533,138 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
5388
6533
|
fetcher: runtime.fetcher ?? fetch,
|
|
5389
6534
|
body,
|
|
5390
6535
|
});
|
|
5391
|
-
printAnalyticsPayload(command, io, payload, [
|
|
6536
|
+
printAnalyticsPayload(command, io, { ...payload, propertySlots }, [
|
|
5392
6537
|
`app_id: ${command.artifactId}`,
|
|
5393
6538
|
`chart_id: ${String(payload.chart?.id ?? "")}`,
|
|
6539
|
+
`property_slots: ${propertySlots.length}`,
|
|
6540
|
+
]);
|
|
6541
|
+
return 0;
|
|
6542
|
+
}
|
|
6543
|
+
async function runAnalyticsChartsList(command, io, runtime) {
|
|
6544
|
+
const { payload } = await requestAnalyticsDashboardsForCli(command, runtime);
|
|
6545
|
+
const dashboard = findAnalyticsDashboard(payload, command.dashboardId);
|
|
6546
|
+
if (!dashboard) {
|
|
6547
|
+
throw new Error(`Analytics dashboard ${command.dashboardId} was not found for app ${command.artifactId}.`);
|
|
6548
|
+
}
|
|
6549
|
+
const charts = readAnalyticsChartRecords(dashboard);
|
|
6550
|
+
const dashboardName = readCliStringField(dashboard, "name");
|
|
6551
|
+
printAnalyticsPayload(command, io, {
|
|
6552
|
+
ok: true,
|
|
6553
|
+
command: command.command,
|
|
6554
|
+
artifactId: command.artifactId,
|
|
6555
|
+
dashboardId: command.dashboardId,
|
|
6556
|
+
dashboard: {
|
|
6557
|
+
id: command.dashboardId,
|
|
6558
|
+
name: dashboardName,
|
|
6559
|
+
},
|
|
6560
|
+
charts,
|
|
6561
|
+
}, [
|
|
6562
|
+
`app_id: ${command.artifactId}`,
|
|
6563
|
+
`dashboard_id: ${command.dashboardId}`,
|
|
6564
|
+
`charts: ${charts.length}`,
|
|
6565
|
+
]);
|
|
6566
|
+
return 0;
|
|
6567
|
+
}
|
|
6568
|
+
async function runAnalyticsChartsUpdate(command, io, runtime) {
|
|
6569
|
+
const { credentials, payload: dashboardsPayload } = await requestAnalyticsDashboardsForCli(command, runtime);
|
|
6570
|
+
const existing = findAnalyticsChart(dashboardsPayload, command.chartId);
|
|
6571
|
+
if (!existing) {
|
|
6572
|
+
throw new Error(`Analytics chart ${command.chartId} was not found for app ${command.artifactId}.`);
|
|
6573
|
+
}
|
|
6574
|
+
const spec = command.specPath
|
|
6575
|
+
? await readAnalyticsSpec({
|
|
6576
|
+
specPath: command.specPath,
|
|
6577
|
+
stdin: runtime.stdin,
|
|
6578
|
+
})
|
|
6579
|
+
: readAnalyticsChartSpec(existing.chart);
|
|
6580
|
+
const title = command.title ?? readAnalyticsChartTitle(existing.chart);
|
|
6581
|
+
const body = {
|
|
6582
|
+
title,
|
|
6583
|
+
spec,
|
|
6584
|
+
layout: readAnalyticsChartLayout(existing.chart),
|
|
6585
|
+
};
|
|
6586
|
+
const propertySlots = await ensureAnalyticsPropertySlots({
|
|
6587
|
+
command,
|
|
6588
|
+
credentials,
|
|
6589
|
+
runtime,
|
|
6590
|
+
requirements: readRequiredAnalyticsPropertySlotsFromSpec(spec),
|
|
6591
|
+
dryRun: command.dryRun,
|
|
6592
|
+
});
|
|
6593
|
+
const dashboardId = readCliStringField(existing.dashboard, "id");
|
|
6594
|
+
if (command.dryRun) {
|
|
6595
|
+
printAnalyticsPayload(command, io, {
|
|
6596
|
+
ok: true,
|
|
6597
|
+
dryRun: true,
|
|
6598
|
+
artifactId: command.artifactId,
|
|
6599
|
+
chartId: command.chartId,
|
|
6600
|
+
dashboardId,
|
|
6601
|
+
currentChart: existing.chart,
|
|
6602
|
+
propertySlots,
|
|
6603
|
+
request: body,
|
|
6604
|
+
}, [
|
|
6605
|
+
`app_id: ${command.artifactId}`,
|
|
6606
|
+
`chart_id: ${command.chartId}`,
|
|
6607
|
+
`title: ${title}`,
|
|
6608
|
+
"dry_run: true",
|
|
6609
|
+
]);
|
|
6610
|
+
return 0;
|
|
6611
|
+
}
|
|
6612
|
+
const payload = await requestAnalyticsApi({
|
|
6613
|
+
artifactId: command.artifactId,
|
|
6614
|
+
analyticsPath: "charts",
|
|
6615
|
+
resourceId: command.chartId,
|
|
6616
|
+
method: "PATCH",
|
|
6617
|
+
baseUrl: credentials.baseUrl,
|
|
6618
|
+
token: credentials.token,
|
|
6619
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6620
|
+
body,
|
|
6621
|
+
});
|
|
6622
|
+
printAnalyticsPayload(command, io, { ...payload, propertySlots }, [
|
|
6623
|
+
`app_id: ${command.artifactId}`,
|
|
6624
|
+
`chart_id: ${command.chartId}`,
|
|
6625
|
+
`title: ${String(payload.chart?.title ?? title)}`,
|
|
6626
|
+
`property_slots: ${propertySlots.length}`,
|
|
6627
|
+
]);
|
|
6628
|
+
return 0;
|
|
6629
|
+
}
|
|
6630
|
+
async function runAnalyticsChartsDelete(command, io, runtime) {
|
|
6631
|
+
const { credentials, payload: dashboardsPayload } = await requestAnalyticsDashboardsForCli(command, runtime);
|
|
6632
|
+
const existing = findAnalyticsChart(dashboardsPayload, command.chartId);
|
|
6633
|
+
if (!existing) {
|
|
6634
|
+
throw new Error(`Analytics chart ${command.chartId} was not found for app ${command.artifactId}.`);
|
|
6635
|
+
}
|
|
6636
|
+
const dashboardId = readCliStringField(existing.dashboard, "id");
|
|
6637
|
+
if (command.dryRun) {
|
|
6638
|
+
printAnalyticsPayload(command, io, {
|
|
6639
|
+
ok: true,
|
|
6640
|
+
dryRun: true,
|
|
6641
|
+
artifactId: command.artifactId,
|
|
6642
|
+
chartId: command.chartId,
|
|
6643
|
+
dashboardId,
|
|
6644
|
+
currentChart: existing.chart,
|
|
6645
|
+
request: {
|
|
6646
|
+
chartId: command.chartId,
|
|
6647
|
+
},
|
|
6648
|
+
}, [
|
|
6649
|
+
`app_id: ${command.artifactId}`,
|
|
6650
|
+
`chart_id: ${command.chartId}`,
|
|
6651
|
+
"dry_run: true",
|
|
6652
|
+
]);
|
|
6653
|
+
return 0;
|
|
6654
|
+
}
|
|
6655
|
+
const payload = await requestAnalyticsApi({
|
|
6656
|
+
artifactId: command.artifactId,
|
|
6657
|
+
analyticsPath: "charts",
|
|
6658
|
+
resourceId: command.chartId,
|
|
6659
|
+
method: "DELETE",
|
|
6660
|
+
baseUrl: credentials.baseUrl,
|
|
6661
|
+
token: credentials.token,
|
|
6662
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6663
|
+
});
|
|
6664
|
+
printAnalyticsPayload(command, io, payload, [
|
|
6665
|
+
`app_id: ${command.artifactId}`,
|
|
6666
|
+
`chart_id: ${command.chartId}`,
|
|
6667
|
+
`deleted: ${String(payload.deleted ?? "")}`,
|
|
5394
6668
|
]);
|
|
5395
6669
|
return 0;
|
|
5396
6670
|
}
|
|
@@ -5503,9 +6777,27 @@ export async function runTenderCli(args, io = {
|
|
|
5503
6777
|
else if (command.topic === "app analytics export query") {
|
|
5504
6778
|
io.stdout(analyticsExportQueryHelp());
|
|
5505
6779
|
}
|
|
6780
|
+
else if (command.topic === "app analytics charts templates") {
|
|
6781
|
+
io.stdout(analyticsChartsTemplatesHelp());
|
|
6782
|
+
}
|
|
5506
6783
|
else if (command.topic === "app analytics charts create") {
|
|
5507
6784
|
io.stdout(analyticsChartsCreateHelp());
|
|
5508
6785
|
}
|
|
6786
|
+
else if (command.topic === "app analytics charts list") {
|
|
6787
|
+
io.stdout(analyticsChartsListHelp());
|
|
6788
|
+
}
|
|
6789
|
+
else if (command.topic === "app analytics charts update") {
|
|
6790
|
+
io.stdout(analyticsChartsUpdateHelp());
|
|
6791
|
+
}
|
|
6792
|
+
else if (command.topic === "app analytics charts delete") {
|
|
6793
|
+
io.stdout(analyticsChartsDeleteHelp());
|
|
6794
|
+
}
|
|
6795
|
+
else if (command.topic === "app analytics property-slots list") {
|
|
6796
|
+
io.stdout(analyticsPropertySlotsListHelp());
|
|
6797
|
+
}
|
|
6798
|
+
else if (command.topic === "app analytics property-slots activate") {
|
|
6799
|
+
io.stdout(analyticsPropertySlotsActivateHelp());
|
|
6800
|
+
}
|
|
5509
6801
|
else if (command.topic === "app") {
|
|
5510
6802
|
io.stdout(appHelp());
|
|
5511
6803
|
}
|
|
@@ -5601,9 +6893,27 @@ export async function runTenderCli(args, io = {
|
|
|
5601
6893
|
if (command.command === "app analytics export query") {
|
|
5602
6894
|
return await runAnalyticsExportQuery(command, io, runtime);
|
|
5603
6895
|
}
|
|
6896
|
+
if (command.command === "app analytics charts templates") {
|
|
6897
|
+
return runAnalyticsChartsTemplates(command, io);
|
|
6898
|
+
}
|
|
5604
6899
|
if (command.command === "app analytics charts create") {
|
|
5605
6900
|
return await runAnalyticsChartsCreate(command, io, runtime);
|
|
5606
6901
|
}
|
|
6902
|
+
if (command.command === "app analytics charts list") {
|
|
6903
|
+
return await runAnalyticsChartsList(command, io, runtime);
|
|
6904
|
+
}
|
|
6905
|
+
if (command.command === "app analytics charts update") {
|
|
6906
|
+
return await runAnalyticsChartsUpdate(command, io, runtime);
|
|
6907
|
+
}
|
|
6908
|
+
if (command.command === "app analytics charts delete") {
|
|
6909
|
+
return await runAnalyticsChartsDelete(command, io, runtime);
|
|
6910
|
+
}
|
|
6911
|
+
if (command.command === "app analytics property-slots list") {
|
|
6912
|
+
return await runAnalyticsPropertySlotsList(command, io, runtime);
|
|
6913
|
+
}
|
|
6914
|
+
if (command.command === "app analytics property-slots activate") {
|
|
6915
|
+
return await runAnalyticsPropertySlotsActivate(command, io, runtime);
|
|
6916
|
+
}
|
|
5607
6917
|
if (command.command === "app git remote") {
|
|
5608
6918
|
return await runGitRemote(command, io, runtime);
|
|
5609
6919
|
}
|