@tenderprompt/cli 0.1.10 → 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 +46 -2
- package/dist/index.js +1060 -40
- 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
|
|
@@ -268,6 +291,11 @@ Commands:
|
|
|
268
291
|
tender app analytics export <kind> <app-id> Export catalog metadata or query rows
|
|
269
292
|
tender app analytics charts templates Print reusable chart spec templates
|
|
270
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
|
|
271
299
|
|
|
272
300
|
Examples:
|
|
273
301
|
tender app analytics summary artifact_123 --range 30d --json
|
|
@@ -279,7 +307,12 @@ Examples:
|
|
|
279
307
|
tender app analytics suggestions artifact_123 --range 2026-04-01..2026-04-30 --json
|
|
280
308
|
tender app analytics dashboards artifact_123 --create "Agent dashboard" --json
|
|
281
309
|
tender app analytics charts templates --json
|
|
282
|
-
tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --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`;
|
|
283
316
|
}
|
|
284
317
|
function analyticsSummaryHelp() {
|
|
285
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]
|
|
@@ -355,16 +388,59 @@ Examples:
|
|
|
355
388
|
function analyticsChartsCreateHelp() {
|
|
356
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]
|
|
357
390
|
|
|
391
|
+
Property charts automatically activate required promoted property slots before
|
|
392
|
+
saving when the spec has an event_name = filter.
|
|
393
|
+
|
|
358
394
|
Examples:
|
|
359
395
|
tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --dry-run --json
|
|
360
396
|
tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json`;
|
|
361
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
|
+
}
|
|
362
438
|
function artifactsListHelp() {
|
|
363
439
|
return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
364
440
|
|
|
365
441
|
Options:
|
|
366
442
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
367
|
-
--token <token> Tender API token.
|
|
443
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
368
444
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
369
445
|
--json Emit stable JSON for coding agents.
|
|
370
446
|
|
|
@@ -381,7 +457,7 @@ Options:
|
|
|
381
457
|
--target-pack <id> Target pack id. Defaults to the API default.
|
|
382
458
|
--target-pack-version <value> Target pack version. Defaults to the API default.
|
|
383
459
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
384
|
-
--token <token> Tender API token.
|
|
460
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
385
461
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
386
462
|
--json Emit stable JSON for coding agents.
|
|
387
463
|
|
|
@@ -395,7 +471,7 @@ function artifactsUpdateHelp() {
|
|
|
395
471
|
Options:
|
|
396
472
|
--name <name> New app name.
|
|
397
473
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
398
|
-
--token <token> Tender API token.
|
|
474
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
399
475
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
400
476
|
--json Emit stable JSON for coding agents.
|
|
401
477
|
|
|
@@ -409,7 +485,7 @@ Options:
|
|
|
409
485
|
--dir <path> Local checkout directory to create/update. Defaults to current directory.
|
|
410
486
|
--remote <name> Git remote name to add or update. Defaults to tender.
|
|
411
487
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
412
|
-
--token <token> Tender API token.
|
|
488
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
413
489
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
414
490
|
--ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
|
|
415
491
|
--dry-run Show file and Git actions without writing.
|
|
@@ -427,7 +503,7 @@ function artifactsContextFetchHelp() {
|
|
|
427
503
|
Options:
|
|
428
504
|
--dir <path> Local checkout to update. Defaults to current directory.
|
|
429
505
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
430
|
-
--token <token> Tender API token.
|
|
506
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
431
507
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
432
508
|
--dry-run Show create/overwrite/conflict actions without writing files.
|
|
433
509
|
--force Overwrite conflicting managed context files.
|
|
@@ -444,7 +520,7 @@ function artifactsContextStatusHelp() {
|
|
|
444
520
|
Options:
|
|
445
521
|
--dir <path> Local checkout to inspect. Defaults to current directory.
|
|
446
522
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
447
|
-
--token <token> Tender API token.
|
|
523
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
448
524
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
449
525
|
--json Emit stable JSON for coding agents.
|
|
450
526
|
|
|
@@ -457,7 +533,7 @@ function artifactsContextRefreshHelp() {
|
|
|
457
533
|
Options:
|
|
458
534
|
--dir <path> Local checkout to update. Defaults to current directory.
|
|
459
535
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
460
|
-
--token <token> Tender API token.
|
|
536
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
461
537
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
462
538
|
--dry-run Show create/overwrite/conflict actions without writing files.
|
|
463
539
|
--force Overwrite conflicting managed context files.
|
|
@@ -514,7 +590,7 @@ function gitRemoteHelp() {
|
|
|
514
590
|
|
|
515
591
|
Options:
|
|
516
592
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
517
|
-
--token <token> Tender API token.
|
|
593
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
518
594
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
519
595
|
--ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
|
|
520
596
|
--json Emit stable JSON for coding agents.
|
|
@@ -531,7 +607,7 @@ Options:
|
|
|
531
607
|
--dir <path> Local Git checkout to configure. Defaults to current directory.
|
|
532
608
|
--remote <name> Git remote name to add or update. Defaults to tender.
|
|
533
609
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
534
|
-
--token <token> Tender API token.
|
|
610
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
535
611
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
536
612
|
--ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
|
|
537
613
|
--dry-run Show Git commands without running them.
|
|
@@ -559,7 +635,7 @@ function previewRebuildHelp() {
|
|
|
559
635
|
Options:
|
|
560
636
|
--commit <sha> Rebuild preview only if the app workspace is at this Git commit.
|
|
561
637
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
562
|
-
--token <token> Tender API token.
|
|
638
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
563
639
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
564
640
|
--stream Request lifecycle events from the API.
|
|
565
641
|
--json Emit stable JSON for coding agents.
|
|
@@ -573,7 +649,7 @@ function validateHelp() {
|
|
|
573
649
|
|
|
574
650
|
Options:
|
|
575
651
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
576
|
-
--token <token> Tender API token.
|
|
652
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
577
653
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
578
654
|
--stream Request lifecycle events from the API.
|
|
579
655
|
--json Emit stable JSON for coding agents.
|
|
@@ -588,7 +664,7 @@ function buildHelp() {
|
|
|
588
664
|
Options:
|
|
589
665
|
--commit <sha> Build only if the app workspace is at this Git commit.
|
|
590
666
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
591
|
-
--token <token> Tender API token.
|
|
667
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
592
668
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
593
669
|
--stream Request lifecycle events from the API.
|
|
594
670
|
--json Emit stable JSON for coding agents.
|
|
@@ -604,7 +680,7 @@ Options:
|
|
|
604
680
|
--commit <sha> Show the latest preview job for one artifact Git commit.
|
|
605
681
|
--job <job-id> Show one lifecycle job directly.
|
|
606
682
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
607
|
-
--token <token> Tender API token.
|
|
683
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
608
684
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
609
685
|
--json Emit stable JSON for coding agents.
|
|
610
686
|
|
|
@@ -619,7 +695,7 @@ Options:
|
|
|
619
695
|
--commit <sha> Watch the latest preview job for one artifact Git commit.
|
|
620
696
|
--job <job-id> Watch one lifecycle job directly.
|
|
621
697
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
622
|
-
--token <token> Tender API token.
|
|
698
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
623
699
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
624
700
|
--stream Request Server-Sent Events from the API.
|
|
625
701
|
--json Emit lifecycle events as JSON lines.
|
|
@@ -637,7 +713,7 @@ Options:
|
|
|
637
713
|
--commit <sha> Publish only if the app workspace is at this Git commit.
|
|
638
714
|
--message <text> Human audit message returned in CLI output.
|
|
639
715
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
640
|
-
--token <token> Tender API token.
|
|
716
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
641
717
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
642
718
|
--stream Request lifecycle events from the API.
|
|
643
719
|
--json Emit stable JSON for coding agents.
|
|
@@ -652,7 +728,7 @@ function publishStatusHelp() {
|
|
|
652
728
|
Options:
|
|
653
729
|
--job <job-id> Show one publish lifecycle job directly.
|
|
654
730
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
655
|
-
--token <token> Tender API token.
|
|
731
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
656
732
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
657
733
|
--json Emit stable JSON for coding agents.
|
|
658
734
|
|
|
@@ -665,7 +741,7 @@ function publishWatchHelp() {
|
|
|
665
741
|
Options:
|
|
666
742
|
--job <job-id> Watch one publish lifecycle job directly.
|
|
667
743
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
668
|
-
--token <token> Tender API token.
|
|
744
|
+
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
669
745
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
670
746
|
--stream Request Server-Sent Events from the API.
|
|
671
747
|
--json Emit lifecycle events as JSON lines.
|
|
@@ -740,12 +816,20 @@ function tenderCliCapabilities() {
|
|
|
740
816
|
"tender app analytics charts templates --json",
|
|
741
817
|
"tender app analytics query <artifact-id> --spec chart.json --json",
|
|
742
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",
|
|
743
821
|
"tender app analytics dashboards <artifact-id> --create \"Agent dashboard\" --dry-run --json",
|
|
744
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",
|
|
745
826
|
],
|
|
746
827
|
notes: [
|
|
747
828
|
"Combine CLI capabilities with local source inspection of analytics calls.",
|
|
748
|
-
"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.",
|
|
749
833
|
],
|
|
750
834
|
},
|
|
751
835
|
{
|
|
@@ -801,6 +885,46 @@ function tenderCliCapabilities() {
|
|
|
801
885
|
requiresAuth: false,
|
|
802
886
|
writes: false,
|
|
803
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
|
+
},
|
|
804
928
|
],
|
|
805
929
|
security: [
|
|
806
930
|
"Prefer artifact-scoped device tokens.",
|
|
@@ -921,6 +1045,19 @@ async function resolveApiCredentials(input) {
|
|
|
921
1045
|
profileName: null,
|
|
922
1046
|
};
|
|
923
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
|
+
}
|
|
924
1061
|
if (input.env.TENDER_API_TOKEN) {
|
|
925
1062
|
return {
|
|
926
1063
|
baseUrl: input.baseUrl ?? readDefaultBaseUrl(input.env),
|
|
@@ -930,7 +1067,7 @@ async function resolveApiCredentials(input) {
|
|
|
930
1067
|
};
|
|
931
1068
|
}
|
|
932
1069
|
const config = await readConfig(input.env);
|
|
933
|
-
const profileName =
|
|
1070
|
+
const profileName = config.defaultProfile ?? DEFAULT_PROFILE_NAME;
|
|
934
1071
|
const profile = config.profiles?.[profileName];
|
|
935
1072
|
if (!profile) {
|
|
936
1073
|
throw new TenderCliUsageError("Tender API token is required. Run `tender auth login --device`, set TENDER_API_TOKEN, or pass --token <token>.");
|
|
@@ -2953,6 +3090,215 @@ function parseAnalyticsChartsCreateArgs(args) {
|
|
|
2953
3090
|
...parsed,
|
|
2954
3091
|
};
|
|
2955
3092
|
}
|
|
3093
|
+
function parseAnalyticsChartsListArgs(args) {
|
|
3094
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3095
|
+
return { command: "help", topic: "app analytics charts list" };
|
|
3096
|
+
}
|
|
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");
|
|
3100
|
+
}
|
|
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;
|
|
3109
|
+
}
|
|
3110
|
+
throw new TenderCliUsageError(`Unknown analytics charts list option: ${arg}`);
|
|
3111
|
+
}
|
|
3112
|
+
if (!dashboardId) {
|
|
3113
|
+
throw new TenderCliUsageError("--dashboard is required. Example: tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json");
|
|
3114
|
+
}
|
|
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" };
|
|
3125
|
+
}
|
|
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");
|
|
3129
|
+
}
|
|
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;
|
|
3141
|
+
}
|
|
3142
|
+
if (arg === "--title") {
|
|
3143
|
+
title = parseRequiredFlagValue(parsed.remaining[index + 1], "--title");
|
|
3144
|
+
index += 1;
|
|
3145
|
+
continue;
|
|
3146
|
+
}
|
|
3147
|
+
if (arg === "--spec") {
|
|
3148
|
+
specPath = parseSpecPath(parsed.remaining[index + 1]);
|
|
3149
|
+
index += 1;
|
|
3150
|
+
continue;
|
|
3151
|
+
}
|
|
3152
|
+
if (arg === "--dry-run") {
|
|
3153
|
+
dryRun = true;
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
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;
|
|
3191
|
+
}
|
|
3192
|
+
if (arg === "--dry-run") {
|
|
3193
|
+
dryRun = true;
|
|
3194
|
+
continue;
|
|
3195
|
+
}
|
|
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;
|
|
3225
|
+
}
|
|
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
|
+
}
|
|
2956
3302
|
function parseTenderArgs(args) {
|
|
2957
3303
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
2958
3304
|
return { command: "help", topic: "root" };
|
|
@@ -3036,6 +3382,23 @@ function parseTenderArgs(args) {
|
|
|
3036
3382
|
if (resourceArgs[1] === "analytics" && resourceArgs[2] === "dashboards") {
|
|
3037
3383
|
return parseAnalyticsDashboardsArgs(resourceArgs.slice(3));
|
|
3038
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
|
+
}
|
|
3039
3402
|
if (resourceArgs[1] === "analytics" &&
|
|
3040
3403
|
resourceArgs[2] === "export" &&
|
|
3041
3404
|
(resourceArgs[3] === undefined ||
|
|
@@ -3070,6 +3433,21 @@ function parseTenderArgs(args) {
|
|
|
3070
3433
|
resourceArgs[3] === "create") {
|
|
3071
3434
|
return parseAnalyticsChartsCreateArgs(resourceArgs.slice(4));
|
|
3072
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
|
+
}
|
|
3073
3451
|
if (resourceArgs[1] === "context" &&
|
|
3074
3452
|
(resourceArgs[2] === undefined ||
|
|
3075
3453
|
resourceArgs[2] === "--help" ||
|
|
@@ -3442,7 +3820,9 @@ async function requestAnalyticsApi(input) {
|
|
|
3442
3820
|
if (input.includeCatalog) {
|
|
3443
3821
|
params.set("includeCatalog", "1");
|
|
3444
3822
|
}
|
|
3445
|
-
const suffix = input.analyticsPath
|
|
3823
|
+
const suffix = input.analyticsPath
|
|
3824
|
+
? `/${input.analyticsPath}${input.resourceId ? `/${encodeURIComponent(input.resourceId)}` : ""}`
|
|
3825
|
+
: "";
|
|
3446
3826
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
3447
3827
|
const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/analytics${suffix}${query}`, {
|
|
3448
3828
|
method: input.method,
|
|
@@ -3460,12 +3840,12 @@ async function requestAnalyticsApi(input) {
|
|
|
3460
3840
|
const message = (typeof payload?.message === "string" && payload.message) ||
|
|
3461
3841
|
(typeof payload?.reasonCode === "string" && payload.reasonCode) ||
|
|
3462
3842
|
`Analytics request failed with HTTP ${response.status}.`;
|
|
3463
|
-
throw new
|
|
3843
|
+
throw new TenderCliAnalyticsApiError([
|
|
3464
3844
|
message,
|
|
3465
3845
|
`Scope: artifact ${input.artifactId}, analytics/${input.analyticsPath || "summary"}.`,
|
|
3466
3846
|
`Reason: ${reasonCode}.`,
|
|
3467
3847
|
`Example: ${analyticsCorrectedExample(input.artifactId, input.analyticsPath)}.`,
|
|
3468
|
-
].join("\n"));
|
|
3848
|
+
].join("\n"), reasonCode);
|
|
3469
3849
|
}
|
|
3470
3850
|
return payload;
|
|
3471
3851
|
}
|
|
@@ -3479,6 +3859,9 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
|
|
|
3479
3859
|
if (analyticsPath === "suggestions") {
|
|
3480
3860
|
return `tender app analytics suggestions ${artifactId} --range 30d --json`;
|
|
3481
3861
|
}
|
|
3862
|
+
if (analyticsPath === "property-slots") {
|
|
3863
|
+
return `tender app analytics property-slots list ${artifactId} --json`;
|
|
3864
|
+
}
|
|
3482
3865
|
if (analyticsPath === "dashboards") {
|
|
3483
3866
|
return `tender app analytics dashboards ${artifactId} --json`;
|
|
3484
3867
|
}
|
|
@@ -3487,6 +3870,332 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
|
|
|
3487
3870
|
}
|
|
3488
3871
|
return `tender app analytics summary ${artifactId} --range 30d --json`;
|
|
3489
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
|
+
}
|
|
3490
4199
|
function printAnalyticsPayload(command, io, payload, fallbackLines) {
|
|
3491
4200
|
if (command.json) {
|
|
3492
4201
|
io.stdout(JSON.stringify(payload, null, 2));
|
|
@@ -3775,6 +4484,47 @@ async function planSourceFiles(input) {
|
|
|
3775
4484
|
}
|
|
3776
4485
|
async function runAuthStatus(command, io, runtime) {
|
|
3777
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
|
+
}
|
|
3778
4528
|
if (env.TENDER_API_TOKEN) {
|
|
3779
4529
|
const result = {
|
|
3780
4530
|
ok: true,
|
|
@@ -5491,14 +6241,11 @@ async function runAnalyticsQuery(command, io, runtime) {
|
|
|
5491
6241
|
specPath: command.specPath,
|
|
5492
6242
|
stdin: runtime.stdin,
|
|
5493
6243
|
});
|
|
5494
|
-
const payload = await
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
token: credentials.token,
|
|
5500
|
-
fetcher: runtime.fetcher ?? fetch,
|
|
5501
|
-
body: { spec },
|
|
6244
|
+
const payload = await requestAnalyticsQueryWithPropertyGuidance({
|
|
6245
|
+
command,
|
|
6246
|
+
credentials,
|
|
6247
|
+
runtime,
|
|
6248
|
+
spec,
|
|
5502
6249
|
});
|
|
5503
6250
|
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
5504
6251
|
printAnalyticsPayload(command, io, payload, [
|
|
@@ -5619,21 +6366,119 @@ async function runAnalyticsExportQuery(command, io, runtime) {
|
|
|
5619
6366
|
specPath: command.specPath,
|
|
5620
6367
|
stdin: runtime.stdin,
|
|
5621
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);
|
|
5622
6385
|
const payload = await requestAnalyticsApi({
|
|
5623
6386
|
artifactId: command.artifactId,
|
|
5624
|
-
analyticsPath: "
|
|
5625
|
-
method: "
|
|
6387
|
+
analyticsPath: "property-slots",
|
|
6388
|
+
method: "GET",
|
|
5626
6389
|
baseUrl: credentials.baseUrl,
|
|
5627
6390
|
token: credentials.token,
|
|
5628
6391
|
fetcher: runtime.fetcher ?? fetch,
|
|
5629
|
-
body: { spec },
|
|
5630
6392
|
});
|
|
5631
|
-
const
|
|
5632
|
-
|
|
5633
|
-
|
|
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
|
+
]);
|
|
5634
6437
|
return 0;
|
|
5635
6438
|
}
|
|
5636
|
-
|
|
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
|
+
]);
|
|
5637
6482
|
return 0;
|
|
5638
6483
|
}
|
|
5639
6484
|
async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
@@ -5647,11 +6492,25 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
5647
6492
|
spec,
|
|
5648
6493
|
layout: {},
|
|
5649
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
|
+
: [];
|
|
5650
6508
|
if (command.dryRun) {
|
|
5651
6509
|
const payload = {
|
|
5652
6510
|
ok: true,
|
|
5653
6511
|
dryRun: true,
|
|
5654
6512
|
artifactId: command.artifactId,
|
|
6513
|
+
propertySlots,
|
|
5655
6514
|
request: body,
|
|
5656
6515
|
};
|
|
5657
6516
|
printAnalyticsPayload(command, io, payload, [
|
|
@@ -5662,7 +6521,9 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
5662
6521
|
]);
|
|
5663
6522
|
return 0;
|
|
5664
6523
|
}
|
|
5665
|
-
|
|
6524
|
+
if (!credentials) {
|
|
6525
|
+
throw new Error("Analytics credentials are required to create saved charts.");
|
|
6526
|
+
}
|
|
5666
6527
|
const payload = await requestAnalyticsApi({
|
|
5667
6528
|
artifactId: command.artifactId,
|
|
5668
6529
|
analyticsPath: "charts",
|
|
@@ -5672,9 +6533,138 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
5672
6533
|
fetcher: runtime.fetcher ?? fetch,
|
|
5673
6534
|
body,
|
|
5674
6535
|
});
|
|
5675
|
-
printAnalyticsPayload(command, io, payload, [
|
|
6536
|
+
printAnalyticsPayload(command, io, { ...payload, propertySlots }, [
|
|
5676
6537
|
`app_id: ${command.artifactId}`,
|
|
5677
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 ?? "")}`,
|
|
5678
6668
|
]);
|
|
5679
6669
|
return 0;
|
|
5680
6670
|
}
|
|
@@ -5793,6 +6783,21 @@ export async function runTenderCli(args, io = {
|
|
|
5793
6783
|
else if (command.topic === "app analytics charts create") {
|
|
5794
6784
|
io.stdout(analyticsChartsCreateHelp());
|
|
5795
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
|
+
}
|
|
5796
6801
|
else if (command.topic === "app") {
|
|
5797
6802
|
io.stdout(appHelp());
|
|
5798
6803
|
}
|
|
@@ -5894,6 +6899,21 @@ export async function runTenderCli(args, io = {
|
|
|
5894
6899
|
if (command.command === "app analytics charts create") {
|
|
5895
6900
|
return await runAnalyticsChartsCreate(command, io, runtime);
|
|
5896
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
|
+
}
|
|
5897
6917
|
if (command.command === "app git remote") {
|
|
5898
6918
|
return await runGitRemote(command, io, runtime);
|
|
5899
6919
|
}
|