@oneuptime/common 10.5.8 → 10.5.17

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/Models/AnalyticsModels/ExceptionInstance.ts +1 -1
  2. package/Models/AnalyticsModels/Log.ts +1 -1
  3. package/Models/AnalyticsModels/Metric.ts +1 -1
  4. package/Models/AnalyticsModels/Profile.ts +1 -1
  5. package/Models/AnalyticsModels/ProfileSample.ts +1 -1
  6. package/Models/AnalyticsModels/Span.ts +1 -1
  7. package/Models/DatabaseModels/SmsLog.ts +111 -0
  8. package/Models/DatabaseModels/TelemetryException.ts +46 -34
  9. package/Models/DatabaseModels/TelemetryUsageBilling.ts +35 -2
  10. package/Server/API/AIAgentDataAPI.ts +25 -7
  11. package/Server/API/DashboardAPI.ts +616 -0
  12. package/Server/API/TelemetryExceptionAPI.ts +6 -2
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.ts +39 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.ts +28 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.ts +24 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.ts +47 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.ts +34 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  19. package/Server/Infrastructure/QueueWorker.ts +40 -1
  20. package/Server/Services/AnalyticsDatabaseService.ts +87 -0
  21. package/Server/Services/DatabaseService.ts +73 -0
  22. package/Server/Services/TelemetryExceptionService.ts +24 -49
  23. package/Server/Services/TelemetryUsageBillingService.ts +289 -166
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +102 -72
  25. package/Server/Types/Database/Permissions/OwnedScopePermission.ts +81 -60
  26. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +67 -0
  27. package/Server/Utils/Logger.ts +12 -1
  28. package/Server/Utils/StartServer.ts +13 -5
  29. package/Server/Utils/Telemetry/ContextSpanProcessor.ts +48 -0
  30. package/Server/Utils/Telemetry/SpanUtil.ts +16 -35
  31. package/Server/Utils/Telemetry/TelemetryContext.ts +190 -0
  32. package/Server/Utils/Telemetry.ts +18 -2
  33. package/Types/Database/AccessControl/OwnedThrough.ts +31 -3
  34. package/Types/SmsStatus.ts +16 -0
  35. package/Types/Telemetry/ServiceType.ts +10 -0
  36. package/UI/Components/LogsViewer/LogsViewer.tsx +16 -0
  37. package/UI/Utils/Project.ts +6 -0
  38. package/UI/Utils/Telemetry/Telemetry.ts +65 -0
  39. package/UI/Utils/TelemetryService.ts +150 -0
  40. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -1
  41. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  42. package/build/dist/Models/AnalyticsModels/Log.js +1 -1
  43. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  44. package/build/dist/Models/AnalyticsModels/Metric.js +1 -1
  45. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  46. package/build/dist/Models/AnalyticsModels/Profile.js +1 -1
  47. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  48. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -1
  49. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  50. package/build/dist/Models/AnalyticsModels/Span.js +1 -1
  51. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  52. package/build/dist/Models/DatabaseModels/SmsLog.js +112 -0
  53. package/build/dist/Models/DatabaseModels/SmsLog.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/TelemetryException.js +47 -33
  55. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  56. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +36 -2
  57. package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
  58. package/build/dist/Server/API/AIAgentDataAPI.js +24 -8
  59. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  60. package/build/dist/Server/API/DashboardAPI.js +459 -2
  61. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  62. package/build/dist/Server/API/TelemetryExceptionAPI.js +6 -2
  63. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.js +20 -0
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.js.map +1 -0
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js +23 -0
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780381124553-MigrationName.js.map +1 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js +19 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780382837019-MigrationName.js.map +1 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js +22 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780387560604-MigrationName.js.map +1 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js +25 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780388219225-MigrationName.js.map +1 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  76. package/build/dist/Server/Infrastructure/QueueWorker.js +31 -1
  77. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  78. package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -0
  79. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  80. package/build/dist/Server/Services/DatabaseService.js +62 -0
  81. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  82. package/build/dist/Server/Services/TelemetryExceptionService.js +16 -41
  83. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  84. package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -147
  85. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  86. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +84 -63
  87. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  88. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js +67 -49
  89. package/build/dist/Server/Types/Database/Permissions/OwnedScopePermission.js.map +1 -1
  90. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +51 -0
  91. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  92. package/build/dist/Server/Utils/Logger.js +8 -1
  93. package/build/dist/Server/Utils/Logger.js.map +1 -1
  94. package/build/dist/Server/Utils/StartServer.js +12 -4
  95. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  96. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js +37 -0
  97. package/build/dist/Server/Utils/Telemetry/ContextSpanProcessor.js.map +1 -0
  98. package/build/dist/Server/Utils/Telemetry/SpanUtil.js +15 -24
  99. package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
  100. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js +124 -0
  101. package/build/dist/Server/Utils/Telemetry/TelemetryContext.js.map +1 -0
  102. package/build/dist/Server/Utils/Telemetry.js +12 -1
  103. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  104. package/build/dist/Types/Database/AccessControl/OwnedThrough.js +7 -2
  105. package/build/dist/Types/Database/AccessControl/OwnedThrough.js.map +1 -1
  106. package/build/dist/Types/SmsStatus.js +15 -0
  107. package/build/dist/Types/SmsStatus.js.map +1 -1
  108. package/build/dist/Types/Telemetry/ServiceType.js +10 -0
  109. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  110. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -0
  111. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  112. package/build/dist/UI/Utils/Project.js +5 -0
  113. package/build/dist/UI/Utils/Project.js.map +1 -1
  114. package/build/dist/UI/Utils/Telemetry/Telemetry.js +44 -0
  115. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  116. package/build/dist/UI/Utils/TelemetryService.js +113 -0
  117. package/build/dist/UI/Utils/TelemetryService.js.map +1 -0
  118. package/package.json +2 -2
@@ -26,6 +26,219 @@ import ForbiddenException from "../../Types/Exception/ForbiddenException";
26
26
  import JSONFunctions from "../../Types/JSONFunctions";
27
27
  import TelemetryAttributeService from "../Services/TelemetryAttributeService";
28
28
  import TelemetryType from "../../Types/Telemetry/TelemetryType";
29
+ import { JSONObject } from "../../Types/JSON";
30
+ import AggregateBy from "../Types/AnalyticsDatabase/AggregateBy";
31
+ import AggregatedResult from "../../Types/BaseDatabase/AggregatedResult";
32
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
33
+ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
34
+ import Metric from "../../Models/AnalyticsModels/Metric";
35
+ import MetricType from "../../Models/DatabaseModels/MetricType";
36
+ import MetricService from "../Services/MetricService";
37
+ import MetricTypeService from "../Services/MetricTypeService";
38
+ import PositiveNumber from "../../Types/PositiveNumber";
39
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
40
+ import AnalyticsDataModel from "../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
41
+ import Incident from "../../Models/DatabaseModels/Incident";
42
+ import Alert from "../../Models/DatabaseModels/Alert";
43
+ import Monitor from "../../Models/DatabaseModels/Monitor";
44
+ import Host from "../../Models/DatabaseModels/Host";
45
+ import KubernetesResource from "../../Models/DatabaseModels/KubernetesResource";
46
+ import DockerHost from "../../Models/DatabaseModels/DockerHost";
47
+ import DockerResource from "../../Models/DatabaseModels/DockerResource";
48
+ import Span from "../../Models/AnalyticsModels/Span";
49
+ import Log from "../../Models/AnalyticsModels/Log";
50
+ import IncidentService from "../Services/IncidentService";
51
+ import AlertService from "../Services/AlertService";
52
+ import MonitorService from "../Services/MonitorService";
53
+ import HostService from "../Services/HostService";
54
+ import KubernetesResourceService from "../Services/KubernetesResourceService";
55
+ import DockerHostService from "../Services/DockerHostService";
56
+ import DockerResourceService from "../Services/DockerResourceService";
57
+ import SpanService from "../Services/SpanService";
58
+ import LogService from "../Services/LogService";
59
+
60
+ /*
61
+ * Registry of the non-metric widgets a public dashboard may render. The
62
+ * `select` is the FIXED set of columns the corresponding widget displays —
63
+ * the public list endpoint ignores any client-supplied select and uses this,
64
+ * so an anonymous viewer can only ever read these columns (and only for the
65
+ * dashboard's own project). Adding a widget to a public dashboard is the
66
+ * owner's explicit opt-in to exposing these columns.
67
+ */
68
+ interface PublicDashboardResourceConfig {
69
+ modelType: { new (): BaseModel | AnalyticsDataModel };
70
+ service: {
71
+ findBy: (findBy: any) => Promise<Array<BaseModel | AnalyticsDataModel>>;
72
+ };
73
+ select: JSONObject;
74
+ }
75
+
76
+ const DEFAULT_DASHBOARD_RESOURCE_LIMIT: number = 100;
77
+
78
+ const PUBLIC_DASHBOARD_RESOURCES: Record<
79
+ string,
80
+ PublicDashboardResourceConfig
81
+ > = {
82
+ incident: {
83
+ modelType: Incident,
84
+ service: IncidentService,
85
+ select: {
86
+ _id: true,
87
+ title: true,
88
+ createdAt: true,
89
+ currentIncidentState: { name: true, color: true },
90
+ incidentSeverity: { name: true, color: true },
91
+ },
92
+ },
93
+ alert: {
94
+ modelType: Alert,
95
+ service: AlertService,
96
+ select: {
97
+ _id: true,
98
+ title: true,
99
+ createdAt: true,
100
+ currentAlertState: { name: true, color: true },
101
+ alertSeverity: { name: true, color: true },
102
+ },
103
+ },
104
+ monitor: {
105
+ modelType: Monitor,
106
+ service: MonitorService,
107
+ select: {
108
+ _id: true,
109
+ name: true,
110
+ monitorType: true,
111
+ currentMonitorStatus: { name: true, color: true },
112
+ },
113
+ },
114
+ host: {
115
+ modelType: Host,
116
+ service: HostService,
117
+ select: {
118
+ _id: true,
119
+ name: true,
120
+ hostIdentifier: true,
121
+ otelCollectorStatus: true,
122
+ osType: true,
123
+ osVersion: true,
124
+ cpuCores: true,
125
+ totalMemoryBytes: true,
126
+ lastSeenAt: true,
127
+ },
128
+ },
129
+ "kubernetes-resource": {
130
+ modelType: KubernetesResource,
131
+ service: KubernetesResourceService,
132
+ select: {
133
+ _id: true,
134
+ name: true,
135
+ namespaceKey: true,
136
+ kind: true,
137
+ phase: true,
138
+ isReady: true,
139
+ hasMemoryPressure: true,
140
+ hasDiskPressure: true,
141
+ hasPidPressure: true,
142
+ containerCount: true,
143
+ latestCpuPercent: true,
144
+ latestMemoryBytes: true,
145
+ controllerDeploymentName: true,
146
+ controllerCronJobName: true,
147
+ resourceCreationTimestamp: true,
148
+ lastSeenAt: true,
149
+ kubernetesClusterId: true,
150
+ kubernetesCluster: { name: true },
151
+ },
152
+ },
153
+ "docker-host": {
154
+ modelType: DockerHost,
155
+ service: DockerHostService,
156
+ select: {
157
+ _id: true,
158
+ name: true,
159
+ otelCollectorStatus: true,
160
+ containersRunning: true,
161
+ containersStopped: true,
162
+ containersPaused: true,
163
+ osType: true,
164
+ osVersion: true,
165
+ },
166
+ },
167
+ "docker-container": {
168
+ modelType: DockerResource,
169
+ service: DockerResourceService,
170
+ select: {
171
+ _id: true,
172
+ name: true,
173
+ imageName: true,
174
+ state: true,
175
+ latestCpuPercent: true,
176
+ latestMemoryBytes: true,
177
+ dockerHostId: true,
178
+ dockerHost: { name: true },
179
+ },
180
+ },
181
+ "docker-image": {
182
+ modelType: DockerResource,
183
+ service: DockerResourceService,
184
+ select: {
185
+ _id: true,
186
+ name: true,
187
+ containerId: true,
188
+ dockerHostId: true,
189
+ dockerHost: { name: true },
190
+ },
191
+ },
192
+ "docker-network": {
193
+ modelType: DockerResource,
194
+ service: DockerResourceService,
195
+ select: {
196
+ _id: true,
197
+ name: true,
198
+ state: true,
199
+ dockerHostId: true,
200
+ dockerHost: { name: true },
201
+ },
202
+ },
203
+ "docker-volume": {
204
+ modelType: DockerResource,
205
+ service: DockerResourceService,
206
+ select: {
207
+ _id: true,
208
+ name: true,
209
+ state: true,
210
+ dockerHostId: true,
211
+ dockerHost: { name: true },
212
+ },
213
+ },
214
+ span: {
215
+ modelType: Span,
216
+ service: SpanService,
217
+ select: {
218
+ startTime: true,
219
+ name: true,
220
+ statusCode: true,
221
+ durationUnixNano: true,
222
+ traceId: true,
223
+ spanId: true,
224
+ kind: true,
225
+ serviceId: true,
226
+ },
227
+ },
228
+ log: {
229
+ modelType: Log,
230
+ service: LogService,
231
+ select: {
232
+ time: true,
233
+ severityText: true,
234
+ body: true,
235
+ serviceId: true,
236
+ traceId: true,
237
+ spanId: true,
238
+ attributes: true,
239
+ },
240
+ },
241
+ };
29
242
 
30
243
  export default class DashboardAPI extends BaseAPI<
31
244
  Dashboard,
@@ -392,6 +605,268 @@ export default class DashboardAPI extends BaseAPI<
392
605
  },
393
606
  );
394
607
 
608
+ /*
609
+ * Public metric-type lookup for dashboard charts.
610
+ *
611
+ * The private `/metric-type/get-list` CRUD route requires a logged-in
612
+ * session with Telemetry read permission; public dashboards have no
613
+ * session, so the shared chart code used to fall through to it, 401 →
614
+ * the global API error handler redirected the viewer to /accounts/login.
615
+ * Mirror it here scoped to the dashboard's owning projectId.
616
+ * Authorization reuses DashboardService.hasReadAccess (public flag, IP
617
+ * whitelist, master password) — never falls back to project-wide read.
618
+ */
619
+ this.router.post(
620
+ `${new this.entityType()
621
+ .getCrudApiPath()
622
+ ?.toString()}/metric-types/:dashboardId`,
623
+ UserMiddleware.getUserMiddleware,
624
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
625
+ try {
626
+ const dashboardId: ObjectID = new ObjectID(
627
+ req.params["dashboardId"] as string,
628
+ );
629
+
630
+ const accessResult: {
631
+ hasReadAccess: boolean;
632
+ error?: NotAuthenticatedException | ForbiddenException;
633
+ } = await DashboardService.hasReadAccess({
634
+ dashboardId,
635
+ req,
636
+ });
637
+
638
+ if (!accessResult.hasReadAccess) {
639
+ throw (
640
+ accessResult.error ||
641
+ new BadDataException("Access denied to this dashboard.")
642
+ );
643
+ }
644
+
645
+ const dashboard: Dashboard | null =
646
+ await DashboardService.findOneById({
647
+ id: dashboardId,
648
+ select: {
649
+ _id: true,
650
+ projectId: true,
651
+ dashboardViewConfig: true,
652
+ },
653
+ props: {
654
+ isRoot: true,
655
+ },
656
+ });
657
+
658
+ if (!dashboard || !dashboard.projectId) {
659
+ throw new NotFoundException("Dashboard not found");
660
+ }
661
+
662
+ /*
663
+ * Only expose metric types the dashboard actually charts, so a
664
+ * public viewer cannot enumerate the owning project's full metric
665
+ * catalog.
666
+ */
667
+ const allowedMetricNames: Set<string> =
668
+ DashboardAPI.collectDashboardMetricNames(
669
+ dashboard.dashboardViewConfig,
670
+ );
671
+
672
+ if (allowedMetricNames.size === 0) {
673
+ return Response.sendJsonObjectResponse(req, res, {
674
+ metricTypes: [],
675
+ });
676
+ }
677
+
678
+ const metricTypes: Array<MetricType> = await MetricTypeService.findBy(
679
+ {
680
+ query: {
681
+ projectId: dashboard.projectId,
682
+ },
683
+ select: {
684
+ name: true,
685
+ unit: true,
686
+ },
687
+ skip: 0,
688
+ limit: LIMIT_PER_PROJECT,
689
+ sort: {
690
+ name: SortOrder.Ascending,
691
+ },
692
+ props: {
693
+ isRoot: true,
694
+ },
695
+ },
696
+ );
697
+
698
+ return Response.sendJsonObjectResponse(req, res, {
699
+ metricTypes: metricTypes
700
+ .filter((metricType: MetricType) => {
701
+ return Boolean(
702
+ metricType.name && allowedMetricNames.has(metricType.name),
703
+ );
704
+ })
705
+ .map((metricType: MetricType) => {
706
+ return {
707
+ name: metricType.name || "",
708
+ unit: metricType.unit || "",
709
+ };
710
+ }),
711
+ });
712
+ } catch (err) {
713
+ next(err);
714
+ }
715
+ },
716
+ );
717
+
718
+ /*
719
+ * Public metric aggregation for dashboard charts/values/gauges/tables.
720
+ *
721
+ * Mirrors the private `/metrics/aggregate` route (which requires a
722
+ * logged-in session). The client-supplied projectId is IGNORED and the
723
+ * aggregation is pinned to the dashboard's owning projectId, so a public
724
+ * viewer can only read metrics belonging to this dashboard's project and
725
+ * never another tenant's. Authorization reuses
726
+ * DashboardService.hasReadAccess.
727
+ */
728
+ this.router.post(
729
+ `${new this.entityType()
730
+ .getCrudApiPath()
731
+ ?.toString()}/metrics-aggregate/:dashboardId`,
732
+ UserMiddleware.getUserMiddleware,
733
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
734
+ try {
735
+ const dashboardId: ObjectID = new ObjectID(
736
+ req.params["dashboardId"] as string,
737
+ );
738
+
739
+ const accessResult: {
740
+ hasReadAccess: boolean;
741
+ error?: NotAuthenticatedException | ForbiddenException;
742
+ } = await DashboardService.hasReadAccess({
743
+ dashboardId,
744
+ req,
745
+ });
746
+
747
+ if (!accessResult.hasReadAccess) {
748
+ throw (
749
+ accessResult.error ||
750
+ new BadDataException("Access denied to this dashboard.")
751
+ );
752
+ }
753
+
754
+ if (!req.body || !req.body["aggregateBy"]) {
755
+ throw new BadDataException("aggregateBy is required.");
756
+ }
757
+
758
+ const dashboard: Dashboard | null =
759
+ await DashboardService.findOneById({
760
+ id: dashboardId,
761
+ select: {
762
+ _id: true,
763
+ projectId: true,
764
+ dashboardViewConfig: true,
765
+ },
766
+ props: {
767
+ isRoot: true,
768
+ },
769
+ });
770
+
771
+ if (!dashboard || !dashboard.projectId) {
772
+ throw new NotFoundException("Dashboard not found");
773
+ }
774
+
775
+ const aggregateBy: AggregateBy<Metric> = {
776
+ ...(JSONFunctions.deserialize(
777
+ req.body["aggregateBy"] as JSONObject,
778
+ ) as unknown as AggregateBy<Metric>),
779
+ /*
780
+ * Run as root: authorization is already enforced by hasReadAccess
781
+ * above and the project scope is pinned below.
782
+ */
783
+ props: {
784
+ isRoot: true,
785
+ },
786
+ };
787
+
788
+ /*
789
+ * Restrict aggregation to the metric names this dashboard actually
790
+ * charts. Without this, a public viewer who knows the dashboard ID
791
+ * could aggregate any metric in the owning project. Variable
792
+ * interpolation only rewrites attribute filters, never the metric
793
+ * name, so the stored view config is an exact allowlist.
794
+ */
795
+ const allowedMetricNames: Set<string> =
796
+ DashboardAPI.collectDashboardMetricNames(
797
+ dashboard.dashboardViewConfig,
798
+ );
799
+ const requestedMetricName: unknown = aggregateBy.query
800
+ ? (aggregateBy.query as Record<string, unknown>)["name"]
801
+ : undefined;
802
+
803
+ if (
804
+ typeof requestedMetricName !== "string" ||
805
+ !allowedMetricNames.has(requestedMetricName)
806
+ ) {
807
+ throw new BadDataException(
808
+ "This metric is not part of this dashboard.",
809
+ );
810
+ }
811
+
812
+ /*
813
+ * Security: never trust a client-supplied projectId on a public,
814
+ * unauthenticated endpoint. Pin the aggregation to the dashboard's
815
+ * project before it reaches the database service.
816
+ */
817
+ aggregateBy.query = {
818
+ ...(aggregateBy.query || {}),
819
+ projectId: dashboard.projectId,
820
+ };
821
+
822
+ const aggregateResult: AggregatedResult =
823
+ await MetricService.aggregateBy(aggregateBy);
824
+
825
+ const responseBody: JSONObject = {
826
+ ...(aggregateResult as unknown as JSONObject),
827
+ };
828
+
829
+ return Response.sendJsonObjectResponse(req, res, responseBody);
830
+ } catch (err) {
831
+ next(err);
832
+ }
833
+ },
834
+ );
835
+
836
+ /*
837
+ * Public resource lists for non-metric dashboard widgets (incident /
838
+ * alert / monitor / trace / log / kubernetes / docker / host lists).
839
+ *
840
+ * Each widget renders a fixed set of columns; the server pins the select
841
+ * to exactly those columns (see PUBLIC_DASHBOARD_RESOURCES) and forces the
842
+ * project scope to the dashboard's project, so a public viewer can only
843
+ * read the data the widget was built to show, for this dashboard's
844
+ * project. Authorization reuses DashboardService.hasReadAccess.
845
+ */
846
+ this.router.post(
847
+ `${new this.entityType()
848
+ .getCrudApiPath()
849
+ ?.toString()}/resource-list/:dashboardId/:resourceType`,
850
+ UserMiddleware.getUserMiddleware,
851
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
852
+ try {
853
+ const resourceType: string = req.params["resourceType"] as string;
854
+ const config: PublicDashboardResourceConfig | undefined =
855
+ PUBLIC_DASHBOARD_RESOURCES[resourceType];
856
+
857
+ if (!config) {
858
+ throw new BadDataException(
859
+ `Unsupported dashboard resource type: ${resourceType}`,
860
+ );
861
+ }
862
+
863
+ await DashboardAPI.servePublicResourceList({ req, res, config });
864
+ } catch (err) {
865
+ next(err);
866
+ }
867
+ },
868
+ );
869
+
395
870
  this.router.post(
396
871
  `${new this.entityType()
397
872
  .getCrudApiPath()
@@ -469,6 +944,147 @@ export default class DashboardAPI extends BaseAPI<
469
944
  );
470
945
  }
471
946
 
947
+ /*
948
+ * Walk a stored dashboard view config and collect every metric name it
949
+ * references (chart/value/gauge/table widgets all carry their metric in a
950
+ * `metricName` field somewhere under their query config). Used to build an
951
+ * allowlist for the public metric endpoints so an anonymous viewer can only
952
+ * read the metrics this dashboard was built to show.
953
+ */
954
+ private static collectDashboardMetricNames(
955
+ dashboardViewConfig: unknown,
956
+ ): Set<string> {
957
+ const metricNames: Set<string> = new Set<string>();
958
+
959
+ const walk: (node: unknown) => void = (node: unknown): void => {
960
+ if (!node || typeof node !== "object") {
961
+ return;
962
+ }
963
+
964
+ if (Array.isArray(node)) {
965
+ for (const item of node) {
966
+ walk(item);
967
+ }
968
+ return;
969
+ }
970
+
971
+ const obj: Record<string, unknown> = node as Record<string, unknown>;
972
+
973
+ const metricName: unknown = obj["metricName"];
974
+ if (typeof metricName === "string" && metricName.trim().length > 0) {
975
+ metricNames.add(metricName);
976
+ }
977
+
978
+ for (const key of Object.keys(obj)) {
979
+ walk(obj[key]);
980
+ }
981
+ };
982
+
983
+ walk(dashboardViewConfig);
984
+
985
+ return metricNames;
986
+ }
987
+
988
+ /*
989
+ * Shared handler for the public resource-list endpoint. Loads the
990
+ * dashboard, enforces read access, pins the query to the dashboard's
991
+ * project, and lists the resource using the registry's FIXED select.
992
+ */
993
+ private static async servePublicResourceList(data: {
994
+ req: ExpressRequest;
995
+ res: ExpressResponse;
996
+ config: PublicDashboardResourceConfig;
997
+ }): Promise<void> {
998
+ const { req, res, config } = data;
999
+
1000
+ const dashboardId: ObjectID = new ObjectID(
1001
+ req.params["dashboardId"] as string,
1002
+ );
1003
+
1004
+ const accessResult: {
1005
+ hasReadAccess: boolean;
1006
+ error?: NotAuthenticatedException | ForbiddenException;
1007
+ } = await DashboardService.hasReadAccess({ dashboardId, req });
1008
+
1009
+ if (!accessResult.hasReadAccess) {
1010
+ throw (
1011
+ accessResult.error ||
1012
+ new BadDataException("Access denied to this dashboard.")
1013
+ );
1014
+ }
1015
+
1016
+ const dashboard: Dashboard | null = await DashboardService.findOneById({
1017
+ id: dashboardId,
1018
+ select: {
1019
+ _id: true,
1020
+ projectId: true,
1021
+ },
1022
+ props: {
1023
+ isRoot: true,
1024
+ },
1025
+ });
1026
+
1027
+ if (!dashboard || !dashboard.projectId) {
1028
+ throw new NotFoundException("Dashboard not found");
1029
+ }
1030
+
1031
+ const query: JSONObject =
1032
+ req.body && req.body["query"]
1033
+ ? (JSONFunctions.deserialize(
1034
+ req.body["query"] as JSONObject,
1035
+ ) as JSONObject)
1036
+ : {};
1037
+
1038
+ /*
1039
+ * Security: pin to the dashboard's project; never trust a client-supplied
1040
+ * projectId on a public, unauthenticated endpoint.
1041
+ */
1042
+ (query as Record<string, unknown>)["projectId"] = dashboard.projectId;
1043
+
1044
+ const sort: JSONObject =
1045
+ req.body && req.body["sort"]
1046
+ ? (JSONFunctions.deserialize(
1047
+ req.body["sort"] as JSONObject,
1048
+ ) as JSONObject)
1049
+ : {};
1050
+
1051
+ const requestedLimit: number = req.query["limit"]
1052
+ ? parseInt(req.query["limit"] as string, 10)
1053
+ : DEFAULT_DASHBOARD_RESOURCE_LIMIT;
1054
+ const limit: number = Math.min(
1055
+ Number.isFinite(requestedLimit) && requestedLimit > 0
1056
+ ? requestedLimit
1057
+ : DEFAULT_DASHBOARD_RESOURCE_LIMIT,
1058
+ LIMIT_PER_PROJECT,
1059
+ );
1060
+
1061
+ const requestedSkip: number = req.query["skip"]
1062
+ ? parseInt(req.query["skip"] as string, 10)
1063
+ : 0;
1064
+ const skip: number =
1065
+ Number.isFinite(requestedSkip) && requestedSkip > 0 ? requestedSkip : 0;
1066
+
1067
+ const list: Array<BaseModel | AnalyticsDataModel> =
1068
+ await config.service.findBy({
1069
+ query: query,
1070
+ select: config.select,
1071
+ sort: sort,
1072
+ limit: limit,
1073
+ skip: skip,
1074
+ props: {
1075
+ isRoot: true,
1076
+ },
1077
+ });
1078
+
1079
+ return Response.sendEntityArrayResponse(
1080
+ req,
1081
+ res,
1082
+ list,
1083
+ new PositiveNumber(list.length),
1084
+ config.modelType,
1085
+ );
1086
+ }
1087
+
472
1088
  private static getFileAsBase64JSONObject(
473
1089
  file: any,
474
1090
  ): { file: string; fileType: string } | null {
@@ -1,5 +1,4 @@
1
1
  import TelemetryException from "../../Models/DatabaseModels/TelemetryException";
2
- import TelemetryServiceModel from "../../Models/DatabaseModels/Service";
3
2
  import AIAgentTask from "../../Models/DatabaseModels/AIAgentTask";
4
3
  import AIAgentTaskTelemetryException from "../../Models/DatabaseModels/AIAgentTaskTelemetryException";
5
4
  import BadDataException from "../../Types/Exception/BadDataException";
@@ -211,8 +210,13 @@ export default class TelemetryExceptionAPI extends BaseAPI<
211
210
 
212
211
  const serviceSummariesJson: JSONArray = summary.serviceSummaries.map(
213
212
  (entry: DashboardServiceSummary): JSONObject => {
213
+ /*
214
+ * serviceId is polymorphic; the client resolves the display name
215
+ * per serviceType (no Service relation to serialize anymore).
216
+ */
214
217
  return {
215
- service: BaseModel.toJSON(entry.service, TelemetryServiceModel),
218
+ serviceId: entry.serviceId,
219
+ serviceType: entry.serviceType,
216
220
  unresolvedCount: entry.unresolvedCount,
217
221
  totalOccurrences: entry.totalOccurrences,
218
222
  };
@@ -0,0 +1,39 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddDeliveryTrackingToSmsLog1780317745887
4
+ implements MigrationInterface
5
+ {
6
+ public name: string = "AddDeliveryTrackingToSmsLog1780317745887";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `ALTER TABLE "SmsLog" ADD "errorCode" character varying(100)`,
11
+ );
12
+ await queryRunner.query(
13
+ `ALTER TABLE "SmsLog" ADD "statusCallbackToken" character varying(100)`,
14
+ );
15
+ await queryRunner.query(
16
+ `ALTER TABLE "SmsLog" ADD "userOnCallLogTimelineId" uuid`,
17
+ );
18
+ await queryRunner.query(
19
+ `CREATE INDEX "IDX_SmsLog_userOnCallLogTimelineId" ON "SmsLog" ("userOnCallLogTimelineId") `,
20
+ );
21
+ await queryRunner.query(
22
+ `ALTER TABLE "SmsLog" ADD CONSTRAINT "FK_SmsLog_userOnCallLogTimelineId" FOREIGN KEY ("userOnCallLogTimelineId") REFERENCES "UserOnCallLogTimeline"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
23
+ );
24
+ }
25
+
26
+ public async down(queryRunner: QueryRunner): Promise<void> {
27
+ await queryRunner.query(
28
+ `ALTER TABLE "SmsLog" DROP CONSTRAINT "FK_SmsLog_userOnCallLogTimelineId"`,
29
+ );
30
+ await queryRunner.query(`DROP INDEX "IDX_SmsLog_userOnCallLogTimelineId"`);
31
+ await queryRunner.query(
32
+ `ALTER TABLE "SmsLog" DROP COLUMN "userOnCallLogTimelineId"`,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "SmsLog" DROP COLUMN "statusCallbackToken"`,
36
+ );
37
+ await queryRunner.query(`ALTER TABLE "SmsLog" DROP COLUMN "errorCode"`);
38
+ }
39
+ }