@finos/legend-query-builder 4.19.12 → 4.19.14

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.
Files changed (118) hide show
  1. package/lib/__lib__/QueryBuilderDocumentation.d.ts +2 -1
  2. package/lib/__lib__/QueryBuilderDocumentation.d.ts.map +1 -1
  3. package/lib/__lib__/QueryBuilderDocumentation.js +1 -0
  4. package/lib/__lib__/QueryBuilderDocumentation.js.map +1 -1
  5. package/lib/__lib__/QueryBuilderEvent.d.ts +63 -2
  6. package/lib/__lib__/QueryBuilderEvent.d.ts.map +1 -1
  7. package/lib/__lib__/QueryBuilderEvent.js +74 -1
  8. package/lib/__lib__/QueryBuilderEvent.js.map +1 -1
  9. package/lib/__lib__/QueryBuilderTelemetryHelper.d.ts +204 -8
  10. package/lib/__lib__/QueryBuilderTelemetryHelper.d.ts.map +1 -1
  11. package/lib/__lib__/QueryBuilderTelemetryHelper.js +104 -0
  12. package/lib/__lib__/QueryBuilderTelemetryHelper.js.map +1 -1
  13. package/lib/components/QueryBuilder.d.ts.map +1 -1
  14. package/lib/components/QueryBuilder.js +83 -14
  15. package/lib/components/QueryBuilder.js.map +1 -1
  16. package/lib/components/QueryBuilderConstantExpressionPanel.d.ts.map +1 -1
  17. package/lib/components/QueryBuilderConstantExpressionPanel.js +18 -1
  18. package/lib/components/QueryBuilderConstantExpressionPanel.js.map +1 -1
  19. package/lib/components/QueryBuilderParametersPanel.d.ts.map +1 -1
  20. package/lib/components/QueryBuilderParametersPanel.js +18 -1
  21. package/lib/components/QueryBuilderParametersPanel.js.map +1 -1
  22. package/lib/components/QueryBuilderSideBar.d.ts.map +1 -1
  23. package/lib/components/QueryBuilderSideBar.js +19 -0
  24. package/lib/components/QueryBuilderSideBar.js.map +1 -1
  25. package/lib/components/fetch-structure/QueryBuilderGraphFetchTreePanel.d.ts.map +1 -1
  26. package/lib/components/fetch-structure/QueryBuilderGraphFetchTreePanel.js +42 -1
  27. package/lib/components/fetch-structure/QueryBuilderGraphFetchTreePanel.js.map +1 -1
  28. package/lib/components/fetch-structure/QueryBuilderPostFilterPanel.d.ts.map +1 -1
  29. package/lib/components/fetch-structure/QueryBuilderPostFilterPanel.js +25 -5
  30. package/lib/components/fetch-structure/QueryBuilderPostFilterPanel.js.map +1 -1
  31. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.d.ts.map +1 -1
  32. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.js +55 -0
  33. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.js.map +1 -1
  34. package/lib/components/fetch-structure/QueryBuilderTDSPanel.d.ts.map +1 -1
  35. package/lib/components/fetch-structure/QueryBuilderTDSPanel.js +95 -8
  36. package/lib/components/fetch-structure/QueryBuilderTDSPanel.js.map +1 -1
  37. package/lib/components/fetch-structure/QueryBuilderTDSWindowPanel.d.ts.map +1 -1
  38. package/lib/components/fetch-structure/QueryBuilderTDSWindowPanel.js +37 -4
  39. package/lib/components/fetch-structure/QueryBuilderTDSWindowPanel.js.map +1 -1
  40. package/lib/components/filter/QueryBuilderFilterPanel.d.ts.map +1 -1
  41. package/lib/components/filter/QueryBuilderFilterPanel.js +24 -5
  42. package/lib/components/filter/QueryBuilderFilterPanel.js.map +1 -1
  43. package/lib/components/result/QueryBuilderDataProductErrorPanel.d.ts +61 -0
  44. package/lib/components/result/QueryBuilderDataProductErrorPanel.d.ts.map +1 -0
  45. package/lib/components/result/QueryBuilderDataProductErrorPanel.js +176 -0
  46. package/lib/components/result/QueryBuilderDataProductErrorPanel.js.map +1 -0
  47. package/lib/components/result/QueryBuilderResultPanel.d.ts.map +1 -1
  48. package/lib/components/result/QueryBuilderResultPanel.js +24 -18
  49. package/lib/components/result/QueryBuilderResultPanel.js.map +1 -1
  50. package/lib/data-access-overview.css +1 -1
  51. package/lib/index.css +2 -2
  52. package/lib/index.css.map +1 -1
  53. package/lib/index.d.ts +2 -1
  54. package/lib/index.d.ts.map +1 -1
  55. package/lib/index.js +1 -0
  56. package/lib/index.js.map +1 -1
  57. package/lib/package.json +1 -1
  58. package/lib/stores/QueryBuilderResultState.d.ts +11 -1
  59. package/lib/stores/QueryBuilderResultState.d.ts.map +1 -1
  60. package/lib/stores/QueryBuilderResultState.js +79 -15
  61. package/lib/stores/QueryBuilderResultState.js.map +1 -1
  62. package/lib/stores/QueryBuilderState.d.ts +139 -9
  63. package/lib/stores/QueryBuilderState.d.ts.map +1 -1
  64. package/lib/stores/QueryBuilderState.js +134 -22
  65. package/lib/stores/QueryBuilderState.js.map +1 -1
  66. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.d.ts +13 -0
  67. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  68. package/lib/stores/QueryLoaderState.d.ts +33 -5
  69. package/lib/stores/QueryLoaderState.d.ts.map +1 -1
  70. package/lib/stores/QueryLoaderState.js +10 -1
  71. package/lib/stores/QueryLoaderState.js.map +1 -1
  72. package/lib/stores/data-access/DataProductAccessInfo.d.ts +37 -0
  73. package/lib/stores/data-access/DataProductAccessInfo.d.ts.map +1 -0
  74. package/lib/stores/data-access/DataProductAccessInfo.js +17 -0
  75. package/lib/stores/data-access/DataProductAccessInfo.js.map +1 -0
  76. package/lib/stores/data-cube/QueryBuilderDataCubeEngine.js +1 -1
  77. package/lib/stores/data-cube/QueryBuilderDataCubeEngine.js.map +1 -1
  78. package/lib/stores/explorer/QueryBuilderExplorerState.js +1 -1
  79. package/lib/stores/explorer/QueryBuilderExplorerState.js.map +1 -1
  80. package/lib/stores/milestoning/QueryBuilderMilestoningHelper.d.ts.map +1 -1
  81. package/lib/stores/milestoning/QueryBuilderMilestoningHelper.js +34 -9
  82. package/lib/stores/milestoning/QueryBuilderMilestoningHelper.js.map +1 -1
  83. package/lib/stores/milestoning/QueryBuilderMilestoningState.d.ts +5 -0
  84. package/lib/stores/milestoning/QueryBuilderMilestoningState.d.ts.map +1 -1
  85. package/lib/stores/milestoning/QueryBuilderMilestoningState.js +13 -0
  86. package/lib/stores/milestoning/QueryBuilderMilestoningState.js.map +1 -1
  87. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.d.ts +2 -0
  88. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.d.ts.map +1 -1
  89. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.js +50 -1
  90. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.js.map +1 -1
  91. package/package.json +10 -10
  92. package/src/__lib__/QueryBuilderDocumentation.ts +1 -0
  93. package/src/__lib__/QueryBuilderEvent.ts +80 -1
  94. package/src/__lib__/QueryBuilderTelemetryHelper.ts +450 -8
  95. package/src/components/QueryBuilder.tsx +157 -38
  96. package/src/components/QueryBuilderConstantExpressionPanel.tsx +24 -2
  97. package/src/components/QueryBuilderParametersPanel.tsx +25 -2
  98. package/src/components/QueryBuilderSideBar.tsx +28 -0
  99. package/src/components/fetch-structure/QueryBuilderGraphFetchTreePanel.tsx +56 -1
  100. package/src/components/fetch-structure/QueryBuilderPostFilterPanel.tsx +31 -4
  101. package/src/components/fetch-structure/QueryBuilderResultModifierPanel.tsx +74 -0
  102. package/src/components/fetch-structure/QueryBuilderTDSPanel.tsx +123 -8
  103. package/src/components/fetch-structure/QueryBuilderTDSWindowPanel.tsx +49 -4
  104. package/src/components/filter/QueryBuilderFilterPanel.tsx +31 -4
  105. package/src/components/result/QueryBuilderDataProductErrorPanel.tsx +441 -0
  106. package/src/components/result/QueryBuilderResultPanel.tsx +49 -32
  107. package/src/index.ts +2 -0
  108. package/src/stores/QueryBuilderResultState.ts +107 -19
  109. package/src/stores/QueryBuilderState.ts +260 -29
  110. package/src/stores/QueryBuilder_LegendApplicationPlugin_Extension.ts +20 -0
  111. package/src/stores/QueryLoaderState.ts +47 -6
  112. package/src/stores/data-access/DataProductAccessInfo.ts +37 -0
  113. package/src/stores/data-cube/QueryBuilderDataCubeEngine.ts +1 -1
  114. package/src/stores/explorer/QueryBuilderExplorerState.ts +1 -1
  115. package/src/stores/milestoning/QueryBuilderMilestoningHelper.ts +63 -8
  116. package/src/stores/milestoning/QueryBuilderMilestoningState.ts +20 -0
  117. package/src/stores/workflows/dataProduct/DataProductQueryBuilderState.ts +68 -0
  118. package/tsconfig.json +2 -0
@@ -24,6 +24,8 @@ import {
24
24
  ActionState,
25
25
  StopWatch,
26
26
  getContentTypeFileExtension,
27
+ buildTelemetryErrorFields,
28
+ type TimingsRecord,
27
29
  } from '@finos/legend-shared';
28
30
  import type { QueryBuilderState } from './QueryBuilderState.js';
29
31
  import {
@@ -49,7 +51,10 @@ import {
49
51
  getExecutionQueryFromRawLambda,
50
52
  } from './shared/LambdaParameterState.js';
51
53
  import type { LambdaFunctionBuilderOption } from './QueryBuilderValueSpecificationBuilderHelper.js';
52
- import { QueryBuilderTelemetryHelper } from '../__lib__/QueryBuilderTelemetryHelper.js';
54
+ import {
55
+ QueryBuilderTelemetryHelper,
56
+ type QueryExecutionFailure_TelemetryData,
57
+ } from '../__lib__/QueryBuilderTelemetryHelper.js';
53
58
  import { QUERY_BUILDER_EVENT } from '../__lib__/QueryBuilderEvent.js';
54
59
  import { ExecutionPlanState } from './execution-plan/ExecutionPlanState.js';
55
60
  import type { DataGridColumnState } from '@finos/legend-lego/data-grid';
@@ -392,6 +397,27 @@ export class QueryBuilderResultState {
392
397
  return query;
393
398
  }
394
399
 
400
+ /**
401
+ * Builds the shared payload for query execution / plan / export failure
402
+ * telemetry, so all four failure callsites emit the same set of dimensions.
403
+ */
404
+ buildFailureTelemetryData(
405
+ error: Error,
406
+ extra?: {
407
+ executionDurationMs?: number | undefined;
408
+ executionTraceId?: string | undefined;
409
+ timings?: TimingsRecord | undefined;
410
+ },
411
+ ): QueryExecutionFailure_TelemetryData {
412
+ return {
413
+ ...buildTelemetryErrorFields(error),
414
+ queryInfo: this.queryBuilderState.safeGetQueryInfo(),
415
+ ...extra,
416
+ ...this.queryBuilderState.safeGetTelemetryContext(),
417
+ ...this.queryBuilderState.safeGetExtraTelemetryMetadata(),
418
+ };
419
+ }
420
+
395
421
  *exportData(format: string): GeneratorFn<void> {
396
422
  try {
397
423
  this.exportState.inProgress();
@@ -450,7 +476,7 @@ export class QueryBuilderResultState {
450
476
  const reportWithState = Object.assign(
451
477
  {},
452
478
  report,
453
- this.queryBuilderState.getStateInfo(),
479
+ this.queryBuilderState.safeGetTelemetryContext(),
454
480
  );
455
481
  QueryBuilderTelemetryHelper.logEvent_ExportQueryDataSucceeded(
456
482
  this.queryBuilderState.applicationStore.telemetryService,
@@ -464,6 +490,13 @@ export class QueryBuilderResultState {
464
490
  LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE),
465
491
  error,
466
492
  );
493
+ QueryBuilderTelemetryHelper.logEvent_ExportQueryDataFailed(
494
+ this.queryBuilderState.applicationStore.telemetryService,
495
+ this.buildFailureTelemetryData(error),
496
+ );
497
+ // without this the action state is left IN_PROGRESS forever when a
498
+ // download fails, unlike the `.then` branch which passes it
499
+ this.exportState.fail();
467
500
  });
468
501
  } catch (error) {
469
502
  this.exportState.fail();
@@ -475,12 +508,26 @@ export class QueryBuilderResultState {
475
508
  this.queryBuilderState.applicationStore.notificationService.notifyError(
476
509
  error,
477
510
  );
478
- this.exportState.complete();
511
+ QueryBuilderTelemetryHelper.logEvent_ExportQueryDataFailed(
512
+ this.queryBuilderState.applicationStore.telemetryService,
513
+ this.buildFailureTelemetryData(error),
514
+ );
515
+ // NOTE: no `complete()` here — it defaults to `hasSucceeded = true`, so
516
+ // calling it after `fail()` above flipped the state back to SUCCEEDED and
517
+ // masked the failure in the UI
479
518
  }
480
519
  }
481
520
 
482
521
  *runQuery(): GeneratorFn<void> {
483
522
  let promise;
523
+ // hoisted so the catch below can report the phase timings collected up to
524
+ // the point of failure — the absence of an engine server-call lap is itself
525
+ // the signal that the query never made it out of the client
526
+ // NOTE: `reportGraphAnalytics` widens the report with `dependenciesCount`;
527
+ // declaring the narrower `GraphManagerOperationReport` here would drop it
528
+ // from the success payload's type
529
+ let report: ReturnType<typeof reportGraphAnalytics> | undefined;
530
+ const stopWatch = new StopWatch();
484
531
  try {
485
532
  this.setIsRunningQuery(true);
486
533
  const currentHashCode = this.queryBuilderState.hashCode;
@@ -502,13 +549,18 @@ export class QueryBuilderResultState {
502
549
  this.queryBuilderState.graphManagerState,
503
550
  );
504
551
 
552
+ stopWatch.record(QUERY_BUILDER_EVENT.RUN_QUERY__PREPARE);
553
+
505
554
  QueryBuilderTelemetryHelper.logEvent_QueryRunLaunched(
506
555
  this.queryBuilderState.applicationStore.telemetryService,
507
- this.queryBuilderState.getExtraTelemetryMetadata(),
556
+ {
557
+ ...this.queryBuilderState.safeGetTelemetryContext(),
558
+ queryInfo: this.queryBuilderState.safeGetQueryInfo(),
559
+ ...this.queryBuilderState.safeGetExtraTelemetryMetadata(),
560
+ },
508
561
  );
509
562
 
510
- const stopWatch = new StopWatch();
511
- const report = reportGraphAnalytics(
563
+ report = reportGraphAnalytics(
512
564
  this.queryBuilderState.graphManagerState.graph,
513
565
  );
514
566
  const contextstate = this.queryBuilderState.executionContextState;
@@ -527,16 +579,25 @@ export class QueryBuilderResultState {
527
579
  floatingExecutionElements:
528
580
  this.queryBuilderState.floatingExecutionElements,
529
581
  },
582
+ // passing the report is what surfaces the graph manager's own engine
583
+ // laps (`V1_ENGINE_OPERATION_INPUT__SUCCESS` / `..._SERVER_CALL__SUCCESS`)
584
+ // in `report.timings` — without it they are computed and discarded
585
+ report,
530
586
  );
531
587
 
532
588
  this.setQueryRunPromise(promise);
533
589
  const result = (yield promise) as ExecutionResultWithMetadata;
534
590
  if (this.queryRunPromise === promise) {
591
+ // close the engine lap without naming it: the graph manager has already
592
+ // recorded that span in `report.timings`, so recording it again here
593
+ // would double-count it
594
+ stopWatch.record();
535
595
  this.processExecutionResult(result.executionResult);
536
596
  if (result.executionTraceId) {
537
597
  this.setExecutionTraceId(result.executionTraceId);
538
598
  }
539
599
  this.latestRunHashCode = currentHashCode;
600
+ stopWatch.record(QUERY_BUILDER_EVENT.RUN_QUERY__PROCESS_RESULT);
540
601
  this.setExecutionDuration(stopWatch.elapsed);
541
602
 
542
603
  report.timings =
@@ -544,12 +605,16 @@ export class QueryBuilderResultState {
544
605
  stopWatch,
545
606
  report.timings,
546
607
  );
547
- const reportWithState = Object.assign(
548
- {},
549
- report,
550
- this.queryBuilderState.getStateInfo(),
551
- this.queryBuilderState.getExtraTelemetryMetadata(),
552
- );
608
+ // NOTE: spread rather than `Object.assign` — with four sources the
609
+ // latter falls through to the `(...sources: any[]) => any` overload,
610
+ // which silently drops type checking on the whole payload
611
+ const reportWithState = {
612
+ ...report,
613
+ ...this.queryBuilderState.safeGetTelemetryContext(),
614
+ ...this.queryBuilderState.safeGetExtraTelemetryMetadata(),
615
+ queryInfo: this.queryBuilderState.safeGetQueryInfo(),
616
+ executionDurationMs: stopWatch.elapsed,
617
+ };
553
618
  QueryBuilderTelemetryHelper.logEvent_QueryRunSucceeded(
554
619
  this.queryBuilderState.applicationStore.telemetryService,
555
620
  reportWithState,
@@ -571,11 +636,18 @@ export class QueryBuilderResultState {
571
636
  }
572
637
  QueryBuilderTelemetryHelper.logEvent_QueryRunFailed(
573
638
  this.queryBuilderState.applicationStore.telemetryService,
574
- {
575
- errorMessage:
576
- error instanceof Error ? error.message : String(error),
577
- ...this.queryBuilderState.getExtraTelemetryMetadata(),
578
- },
639
+ this.buildFailureTelemetryData(error, {
640
+ executionDurationMs: stopWatch.elapsed,
641
+ executionTraceId:
642
+ error instanceof ExecutionError
643
+ ? error.executionTraceId
644
+ : undefined,
645
+ timings:
646
+ this.queryBuilderState.applicationStore.timeService.finalizeTimingsRecord(
647
+ stopWatch,
648
+ report?.timings,
649
+ ),
650
+ }),
579
651
  );
580
652
  }
581
653
  } finally {
@@ -594,7 +666,11 @@ export class QueryBuilderResultState {
594
666
  );
595
667
  QueryBuilderTelemetryHelper.logEvent_QueryRunCancelled(
596
668
  this.queryBuilderState.applicationStore.telemetryService,
597
- this.queryBuilderState.getExtraTelemetryMetadata(),
669
+ {
670
+ ...this.queryBuilderState.safeGetTelemetryContext(),
671
+ queryInfo: this.queryBuilderState.safeGetQueryInfo(),
672
+ ...this.queryBuilderState.safeGetExtraTelemetryMetadata(),
673
+ },
598
674
  );
599
675
  } catch (error) {
600
676
  // Don't notify users about success or failure
@@ -689,7 +765,7 @@ export class QueryBuilderResultState {
689
765
  const reportWithState = Object.assign(
690
766
  {},
691
767
  report,
692
- this.queryBuilderState.getStateInfo(),
768
+ this.queryBuilderState.safeGetTelemetryContext(),
693
769
  );
694
770
  if (debug) {
695
771
  QueryBuilderTelemetryHelper.logEvent_ExecutionPlanDebugSucceeded(
@@ -711,6 +787,18 @@ export class QueryBuilderResultState {
711
787
  this.queryBuilderState.applicationStore.notificationService.notifyError(
712
788
  error,
713
789
  );
790
+ const failurePayload = this.buildFailureTelemetryData(error);
791
+ if (debug) {
792
+ QueryBuilderTelemetryHelper.logEvent_ExecutionPlanDebugFailed(
793
+ this.queryBuilderState.applicationStore.telemetryService,
794
+ failurePayload,
795
+ );
796
+ } else {
797
+ QueryBuilderTelemetryHelper.logEvent_ExecutionPlanGenerationFailed(
798
+ this.queryBuilderState.applicationStore.telemetryService,
799
+ failurePayload,
800
+ );
801
+ }
714
802
  } finally {
715
803
  this.isGeneratingPlan = false;
716
804
  }
@@ -26,6 +26,7 @@ import {
26
26
  type GeneratorFn,
27
27
  LogEvent,
28
28
  assertErrorThrown,
29
+ buildTelemetryErrorFields,
29
30
  guaranteeNonNullable,
30
31
  guaranteeType,
31
32
  filterByType,
@@ -84,9 +85,10 @@ import {
84
85
  buildLambdaFunction,
85
86
  buildExecutionContextState,
86
87
  } from './QueryBuilderValueSpecificationBuilder.js';
87
- import type {
88
- CommandRegistrar,
89
- GenericLegendApplicationStore,
88
+ import {
89
+ type CommandRegistrar,
90
+ type GenericLegendApplicationStore,
91
+ APPLICATION_EVENT,
90
92
  } from '@finos/legend-application';
91
93
  import { QueryFunctionsExplorerState } from './explorer/QueryFunctionsExplorerState.js';
92
94
  import {
@@ -96,13 +98,17 @@ import {
96
98
  import type { QueryBuilderFilterOperator } from './filter/QueryBuilderFilterOperator.js';
97
99
  import { getQueryBuilderCoreFilterOperators } from './filter/QueryBuilderFilterOperatorLoader.js';
98
100
  import { QueryBuilderChangeDetectionState } from './QueryBuilderChangeDetectionState.js';
99
- import { QueryBuilderMilestoningState } from './milestoning/QueryBuilderMilestoningState.js';
101
+ import {
102
+ QueryBuilderMilestoningState,
103
+ type QueryBuilderMilestoningKind,
104
+ } from './milestoning/QueryBuilderMilestoningState.js';
100
105
  import { QUERY_BUILDER_STATE_HASH_STRUCTURE } from './QueryBuilderStateHashUtils.js';
101
106
  import { QUERY_BUILDER_COMMAND_KEY } from './QueryBuilderCommand.js';
102
107
  import { QueryBuilderWatermarkState } from './watermark/QueryBuilderWatermarkState.js';
103
108
  import { QueryBuilderConstantsState } from './QueryBuilderConstantsState.js';
104
109
  import { QueryBuilderCheckEntitlementsState } from './entitlements/QueryBuilderCheckEntitlementsState.js';
105
110
  import { QueryBuilderTDSState } from './fetch-structure/tds/QueryBuilderTDSState.js';
111
+ import { QueryBuilderRelationColumnProjectionColumnState } from './fetch-structure/tds/projection/QueryBuilderProjectionColumnState.js';
106
112
  import {
107
113
  QUERY_BUILDER_PURE_PATH,
108
114
  QUERY_BUILDER_SUPPORTED_GET_ALL_FUNCTIONS,
@@ -114,7 +120,10 @@ import {
114
120
  type QueryBuilderExecutionContextState,
115
121
  } from './QueryBuilderExecutionContextState.js';
116
122
  import type { QueryBuilderConfig } from '../graph-manager/QueryBuilderConfig.js';
117
- import { QUERY_BUILDER_EVENT } from '../__lib__/QueryBuilderEvent.js';
123
+ import {
124
+ QUERY_BUILDER_EVENT,
125
+ type QUERY_BUILDER_OPENED_FROM,
126
+ } from '../__lib__/QueryBuilderEvent.js';
118
127
  import { QUERY_BUILDER_SETTING_KEY } from '../__lib__/QueryBuilderSetting.js';
119
128
  import { QueryBuilderChangeHistoryState } from './QueryBuilderChangeHistoryState.js';
120
129
  import { type QueryBuilderWorkflowState } from './query-workflow/QueryBuilderWorkFlowState.js';
@@ -127,11 +136,87 @@ import type {
127
136
  DepotEntityWithOrigin,
128
137
  QueryableSourceInfo,
129
138
  } from '@finos/legend-storage';
139
+ import type { FETCH_STRUCTURE_IMPLEMENTATION } from './fetch-structure/QueryBuilderFetchStructureImplementationState.js';
130
140
 
131
- export type QueryableClassMappingRuntimeInfo = QueryableSourceInfo & {
132
- class: string;
133
- mapping: string;
134
- runtime: string;
141
+ /**
142
+ * The execution context the query builder has resolved to, as reported in
143
+ * telemetry under the `state` key. See
144
+ * {@link QueryBuilderState.getExecutionContextInfo}.
145
+ */
146
+ export type QueryBuilderExecutionContextInfo = {
147
+ class?: string | undefined;
148
+ mapping?: string | undefined;
149
+ runtime?: string | undefined;
150
+ /**
151
+ * `true` when the query runs against an inline (engineered) runtime rather
152
+ * than a `RuntimePointer`. Those have no element path to report, so `runtime`
153
+ * is absent — this flag keeps that population visible instead of leaving it
154
+ * indistinguishable from "no runtime selected yet".
155
+ */
156
+ isInlineRuntime?: boolean | undefined;
157
+ };
158
+
159
+ /**
160
+ * The shared envelope carried by every query builder telemetry event.
161
+ *
162
+ * The entry point the builder was opened with (`sourceInfo` — the route) is
163
+ * spread *flat* at the top level, while the execution context it resolved to is
164
+ * nested under `state`. Keeping them separate is what lets a dashboard tell
165
+ * "arrived on mapping X" apart from "currently querying mapping X"; merging the
166
+ * two into one flat object would collapse that distinction.
167
+ */
168
+ export type QueryBuilderTelemetryContext = QueryableSourceInfo &
169
+ /**
170
+ * `QueryableSourceInfo` is a marker interface with no declared members — the
171
+ * concrete keys (`sourceType`, `groupId`, `dataSpace`, `dataProduct`, …) vary
172
+ * by entry point and are only known to the application layer. The index
173
+ * signature is what lets those keys sit flat at the top level; it does mean
174
+ * top-level excess-property checking is off. The fields that matter are kept
175
+ * inside the strictly-typed `state` and `change` sub-objects for exactly this
176
+ * reason.
177
+ */
178
+ Record<PropertyKey, unknown> & {
179
+ state?: QueryBuilderExecutionContextInfo | undefined;
180
+ };
181
+
182
+ /**
183
+ * Summary of the current query builder authoring state, used to enrich
184
+ * telemetry (e.g. query-execution events) with lightweight, non-PII shape
185
+ * information such as which fetch structure is in use and how many columns
186
+ * / filters / parameters have been configured.
187
+ */
188
+ export type QueryBuilderQueryInfo = {
189
+ fetchStructureType: FETCH_STRUCTURE_IMPLEMENTATION | string;
190
+ /**
191
+ * `false` when the query lambda could not be built into the form-mode
192
+ * builder and the user landed on the raw-lambda / unsupported-query editor
193
+ * instead. Reported on every event that carries `queryInfo` so dashboards can
194
+ * separate supported-mode activity from unsupported-mode fallback without
195
+ * relying on the (fire-and-forget) `unsupported-query.launch` event alone.
196
+ */
197
+ isQuerySupported: boolean;
198
+ /**
199
+ * `true` when the TDS query is authored against the typed relation function
200
+ * family (`->project`/`->groupBy`/etc. over relation columns). `false` for
201
+ * classic property-driven TDS. `undefined` when the fetch structure is not
202
+ * TDS or when there are no columns to determine yet.
203
+ */
204
+ isTypedFetchStructure?: boolean | undefined;
205
+ parameterCount: number;
206
+ constantCount: number;
207
+ hasFilter: boolean;
208
+ filterNodeCount: number;
209
+ watermarkEnabled: boolean;
210
+ milestoningKind: QueryBuilderMilestoningKind;
211
+ // TDS-specific fields (present when fetchStructureType is TABULAR_DATA_STRUCTURE)
212
+ projectionColumnCount?: number | undefined;
213
+ windowColumnCount?: number | undefined;
214
+ aggregationColumnCount?: number | undefined;
215
+ postFilterNodeCount?: number | undefined;
216
+ hasLimit?: boolean | undefined;
217
+ hasDistinct?: boolean | undefined;
218
+ sortColumnCount?: number | undefined;
219
+ hasSlice?: boolean | undefined;
135
220
  };
136
221
 
137
222
  export type QueryBuilderExtraFunctionAnalysisInfo = {
@@ -525,28 +610,163 @@ export abstract class QueryBuilderState implements CommandRegistrar {
525
610
  }
526
611
 
527
612
  /**
528
- * Gets information about the current queryBuilderState.
529
- * This information can be used as a part of analytics
613
+ * Gets the execution context the query builder has currently *resolved to*:
614
+ * the class being queried and the mapping/runtime it will execute against.
615
+ *
616
+ * This is deliberately distinct from `sourceInfo`, which records what the
617
+ * builder was *opened with* (the route). For a mapping-sourced query both are
618
+ * populated and initially identical, so a divergence means the user switched;
619
+ * for a data space / data product / service query only this side carries a
620
+ * mapping and runtime, because the entry point resolves to one.
621
+ *
622
+ * Degrades gracefully — whichever of class / mapping / runtime are known get
623
+ * reported, so events that fire while the user is still setting up (picking a
624
+ * class before a mapping, say) are not blank. Returns `undefined` only when
625
+ * none of the three has resolved.
530
626
  */
531
- getStateInfo(): QueryableClassMappingRuntimeInfo | undefined {
532
- if (this.sourceInfo) {
533
- const classPath = this.sourceClass?.path;
534
- const mappingPath = this.executionContextState.mapping?.path;
535
- const runtimePath =
536
- this.executionContextState.runtimeValue instanceof RuntimePointer
537
- ? this.executionContextState.runtimeValue.packageableRuntime.value
538
- .path
539
- : undefined;
540
- if (classPath && mappingPath && runtimePath) {
541
- const contextInfo = {
542
- class: classPath,
543
- mapping: mappingPath,
544
- runtime: runtimePath,
545
- };
546
- return Object.assign({}, this.sourceInfo, contextInfo);
547
- }
627
+ getExecutionContextInfo(): QueryBuilderExecutionContextInfo | undefined {
628
+ const classPath = this.sourceClass?.path;
629
+ const mappingPath = this.executionContextState.mapping?.path;
630
+ const runtimeValue = this.executionContextState.runtimeValue;
631
+ const runtimePath =
632
+ runtimeValue instanceof RuntimePointer
633
+ ? runtimeValue.packageableRuntime.value.path
634
+ : undefined;
635
+ const isInlineRuntime =
636
+ runtimeValue !== undefined && !(runtimeValue instanceof RuntimePointer);
637
+ if (!classPath && !mappingPath && !runtimePath && !isInlineRuntime) {
638
+ return undefined;
548
639
  }
549
- return undefined;
640
+ return {
641
+ class: classPath,
642
+ mapping: mappingPath,
643
+ runtime: runtimePath,
644
+ isInlineRuntime: isInlineRuntime ? true : undefined,
645
+ };
646
+ }
647
+
648
+ /**
649
+ * Gets a lightweight snapshot of the current query builder authoring state
650
+ * (fetch structure kind, filter/parameter/constant counts, milestoning /
651
+ * watermark configuration, etc.) for telemetry payloads. Kept intentionally
652
+ * shape-only — no user values, no identifiers.
653
+ */
654
+ getQueryInfo(): QueryBuilderQueryInfo {
655
+ const base: QueryBuilderQueryInfo = {
656
+ fetchStructureType: this.fetchStructureState.implementation.type,
657
+ isQuerySupported: !this.unsupportedQueryState.rawLambda,
658
+ parameterCount: this.parametersState.parameterStates.length,
659
+ constantCount: this.constantState.constants.length,
660
+ hasFilter: !this.filterState.isEmpty,
661
+ filterNodeCount: this.filterState.nodes.size,
662
+ watermarkEnabled: this.watermarkState.value !== undefined,
663
+ milestoningKind: this.milestoningState.milestoningKind,
664
+ };
665
+ if (
666
+ this.fetchStructureState.implementation instanceof QueryBuilderTDSState
667
+ ) {
668
+ const tdsState = this.fetchStructureState.implementation;
669
+ const modifier = tdsState.resultSetModifierState;
670
+ base.isTypedFetchStructure =
671
+ tdsState.projectionColumns.length > 0 &&
672
+ tdsState.projectionColumns.every(
673
+ (col) =>
674
+ col instanceof QueryBuilderRelationColumnProjectionColumnState,
675
+ );
676
+ base.projectionColumnCount = tdsState.projectionColumns.length;
677
+ base.windowColumnCount = tdsState.windowState.windowColumns.length;
678
+ base.aggregationColumnCount = tdsState.aggregationState.columns.length;
679
+ base.postFilterNodeCount = tdsState.postFilterState.nodes.size;
680
+ base.hasLimit = modifier.limit !== undefined;
681
+ base.hasDistinct = modifier.distinct;
682
+ base.sortColumnCount = modifier.sortColumns.length;
683
+ base.hasSlice = modifier.slice !== undefined;
684
+ }
685
+ return base;
686
+ }
687
+
688
+ /**
689
+ * Telemetry must never crash a user-visible action, so every `safeGet*`
690
+ * wrapper below swallows errors from its snapshot builder (state might be
691
+ * mid-construction, or a downstream computed getter might throw in edge
692
+ * cases) and logs them instead of throwing.
693
+ */
694
+ private safeGetTelemetry<T>(getter: () => T, fallback: T, label: string): T {
695
+ try {
696
+ return getter();
697
+ } catch (error) {
698
+ this.applicationStore.logService.warn(
699
+ LogEvent.create(APPLICATION_EVENT.GENERIC_FAILURE),
700
+ label,
701
+ error,
702
+ );
703
+ return fallback;
704
+ }
705
+ }
706
+
707
+ /**
708
+ * Builds the shared telemetry envelope every query builder event carries: the
709
+ * entry point the builder was opened with, spread flat, plus the execution
710
+ * context it resolved to, nested under `state`. Spread this into a payload
711
+ * rather than assembling the two halves by hand at each callsite.
712
+ */
713
+ safeGetTelemetryContext(): QueryBuilderTelemetryContext {
714
+ return this.safeGetTelemetry<QueryBuilderTelemetryContext>(
715
+ () => ({ ...this.sourceInfo, state: this.getExecutionContextInfo() }),
716
+ {},
717
+ 'Failed to build query builder telemetry context',
718
+ );
719
+ }
720
+
721
+ /**
722
+ * Reports that a query builder was opened, from whichever surface opened it.
723
+ * This is the canonical "a query builder exists" signal — hosts should prefer
724
+ * it over their own route-specific load events, which each cover only part of
725
+ * the population.
726
+ *
727
+ * Deliberately called by the host once the builder is *loaded*, rather than
728
+ * emitted from the constructor. The constructor runs before subclass field
729
+ * initializers, so the execution context would be unresolved and the `safeGet*`
730
+ * wrappers would silently report an empty envelope; it would also fire for
731
+ * derived states such as {@link INTERNAL__toBasicQueryBuilderState}, which are
732
+ * built on every data preview and are not user-facing opens.
733
+ *
734
+ * `extra` carries host-specific supplementary fields (load timings, and for
735
+ * Legend Query creators `restoredFromRecent`). It is untyped to keep this
736
+ * package host-agnostic.
737
+ */
738
+ logOpened(
739
+ openedFrom: QUERY_BUILDER_OPENED_FROM,
740
+ extra?: Record<string, unknown> | undefined,
741
+ ): void {
742
+ QueryBuilderTelemetryHelper.logEvent_QueryBuilderOpened(
743
+ this.applicationStore.telemetryService,
744
+ {
745
+ openedFrom,
746
+ ...this.safeGetTelemetryContext(),
747
+ ...extra,
748
+ },
749
+ );
750
+ }
751
+
752
+ /**
753
+ * Prefer this over the raw `getQueryInfo()` method on the hot path of
754
+ * execution telemetry.
755
+ */
756
+ safeGetQueryInfo(): QueryBuilderQueryInfo | undefined {
757
+ return this.safeGetTelemetry(
758
+ () => this.getQueryInfo(),
759
+ undefined,
760
+ 'Failed to build query builder telemetry query info',
761
+ );
762
+ }
763
+
764
+ safeGetExtraTelemetryMetadata(): Record<string, unknown> {
765
+ return this.safeGetTelemetry(
766
+ () => this.getExtraTelemetryMetadata(),
767
+ {},
768
+ 'Failed to build query builder extra telemetry metadata',
769
+ );
550
770
  }
551
771
 
552
772
  setIsAgentChatOpened(val: boolean): void {
@@ -975,6 +1195,17 @@ export abstract class QueryBuilderState implements CommandRegistrar {
975
1195
  LogEvent.create(QUERY_BUILDER_EVENT.UNSUPPORTED_QUERY_LAUNCH),
976
1196
  error,
977
1197
  );
1198
+ // Also route to the telemetry service so unsupported-lambda fallback is
1199
+ // countable per entry point / GAV, not just visible in developer logs.
1200
+ // The full untruncated stack is already in `logService.error` above; this
1201
+ // event carries the shared envelope + capped error dimensions.
1202
+ QueryBuilderTelemetryHelper.logEvent_UnsupportedQueryLaunched(
1203
+ this.applicationStore.telemetryService,
1204
+ {
1205
+ ...this.safeGetTelemetryContext(),
1206
+ ...buildTelemetryErrorFields(error),
1207
+ },
1208
+ );
978
1209
  this.resetQueryResult({ preserveResult: options?.preserveResult });
979
1210
  this.resetQueryContent();
980
1211
  this.unsupportedQueryState.setLambdaError(error);
@@ -21,6 +21,7 @@ import type {
21
21
  DataAccessState,
22
22
  DatasetAccessInfo,
23
23
  } from './data-access/DataAccessState.js';
24
+ import type { DataProductAccessInfo } from './data-access/DataProductAccessInfo.js';
24
25
 
25
26
  export type CuratedTemplateQuery = {
26
27
  id: string;
@@ -67,6 +68,16 @@ export type WarehouseEntitlementRender = {
67
68
  renderer: (dataAccessState: DataAccessState) => React.ReactNode;
68
69
  };
69
70
 
71
+ /**
72
+ * Builds the address where access to a data product can be requested (e.g. its
73
+ * Marketplace page). Returns `undefined` when the host application cannot produce
74
+ * one, in which case no request-access action is offered.
75
+ */
76
+ export type DataProductAccessRequestLinkBuilder = (
77
+ info: DataProductAccessInfo,
78
+ queryBuilderState: QueryBuilderState,
79
+ ) => string | undefined;
80
+
70
81
  export type QueryAgentChatRenderer = (
71
82
  queryBuilderState: QueryBuilderState,
72
83
  ) => React.ReactNode;
@@ -94,6 +105,9 @@ export type QueryBuilderMenuActionConfiguration = {
94
105
  queryBuilderState: QueryBuilderState,
95
106
  ) => React.ReactNode;
96
107
  disableMessage?: string | undefined;
108
+ getDisableMessage?:
109
+ | ((queryBuilderState: QueryBuilderState) => string | undefined)
110
+ | undefined;
97
111
  };
98
112
 
99
113
  export type QueryBuilderPropagateExecutionContextChangeHelper = (
@@ -140,6 +154,12 @@ export interface QueryBuilder_LegendApplicationPlugin_Extension
140
154
  */
141
155
  getExtraDatasetEntitlementAccessNotGrantedReportActionConfigurations?(): DatasetEntitlementAccessReportActionConfiguration[];
142
156
 
157
+ /**
158
+ * Get the list of builders for the address where access to a data product can be
159
+ * requested. The first one to produce an address wins.
160
+ */
161
+ getDataProductAccessRequestLinkBuilders?(): DataProductAccessRequestLinkBuilder[];
162
+
143
163
  /**
144
164
  * Get the list of query usage configurations
145
165
  */