@oneuptime/common 11.5.13 → 11.6.1

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 (156) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
  2. package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
  3. package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
  4. package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
  5. package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
  6. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
  7. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
  8. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  9. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  10. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  11. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  12. package/Server/API/GlobalConfigAPI.ts +62 -1
  13. package/Server/EnvironmentConfig.ts +22 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -46
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/WorkflowService.ts +100 -39
  20. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  21. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  22. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
  23. package/Server/Utils/Express.ts +13 -0
  24. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  25. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  26. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  27. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  28. package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
  29. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  30. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  31. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  32. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
  33. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
  34. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  35. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  36. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  37. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  38. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  39. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  40. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  41. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  42. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  43. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  44. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  45. package/Tests/Types/HashCode.test.ts +41 -0
  46. package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
  47. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  48. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  49. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  50. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  51. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  52. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  53. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  54. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  55. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  56. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  57. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  58. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  59. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  60. package/Tests/Utils/Array.test.ts +156 -0
  61. package/Tests/Utils/CronTab.test.ts +199 -0
  62. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
  63. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  64. package/Tests/Utils/Number.test.ts +179 -0
  65. package/Tests/Utils/ValueFormatter.test.ts +305 -0
  66. package/Tests/Utils/VersionUtil.test.ts +348 -0
  67. package/Tests/jest.setup.ts +1 -0
  68. package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
  69. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  70. package/Types/Monitor/MonitorStep.ts +24 -4
  71. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  72. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  73. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  74. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  75. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  76. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  77. package/Utils/CronTab.ts +823 -0
  78. package/Utils/ModelImportExport.ts +21 -6
  79. package/Utils/VersionUtil.ts +260 -0
  80. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
  81. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
  83. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
  85. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
  86. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
  87. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
  88. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
  89. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
  90. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
  91. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
  92. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
  93. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  100. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  101. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  102. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  103. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +17 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
  113. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/DatabaseService.js +53 -1
  115. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  116. package/build/dist/Server/Services/WorkflowService.js +80 -30
  117. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  118. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  119. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  120. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  121. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/Express.js +12 -0
  125. package/build/dist/Server/Utils/Express.js.map +1 -1
  126. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  127. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  128. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  129. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  131. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  133. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  134. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  135. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  136. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  137. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  139. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  140. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  141. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  142. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  143. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  144. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  145. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  146. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  147. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  148. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  149. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  150. package/build/dist/Utils/CronTab.js +609 -0
  151. package/build/dist/Utils/CronTab.js.map +1 -0
  152. package/build/dist/Utils/ModelImportExport.js +20 -6
  153. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  154. package/build/dist/Utils/VersionUtil.js +193 -0
  155. package/build/dist/Utils/VersionUtil.js.map +1 -0
  156. package/package.json +1 -1
@@ -17,8 +17,14 @@ import API from "../../Utils/API";
17
17
  import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
18
18
  import HTTPResponse from "../../Types/API/HTTPResponse";
19
19
  import PartialEntity from "../../Types/Database/PartialEntity";
20
- import { EnterpriseLicenseValidationUrl, Host } from "../EnvironmentConfig";
20
+ import {
21
+ AppVersion,
22
+ DisableUpdateCheck,
23
+ EnterpriseLicenseValidationUrl,
24
+ Host,
25
+ } from "../EnvironmentConfig";
21
26
  import EnterpriseLicenseInstanceSummary from "../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
27
+ import VersionUtil from "../../Utils/VersionUtil";
22
28
  import UserMiddleware from "../Middleware/UserAuthorization";
23
29
 
24
30
  export default class GlobalConfigAPI extends BaseAPI<
@@ -67,11 +73,15 @@ export default class GlobalConfigAPI extends BaseAPI<
67
73
  enterpriseLicenseExpiresAt: true,
68
74
  enterpriseLicenseKey: true,
69
75
  enterpriseLicenseToken: true,
76
+ enterpriseLicenseIsEvaluation: true,
70
77
  enterpriseLicenseUserLimit: true,
71
78
  enterpriseLicenseCurrentUserCount: true,
72
79
  enterpriseLicenseUserCountUpdatedAt: true,
73
80
  enterpriseLicenseInstances: true,
74
81
  instanceId: true,
82
+ latestReleaseVersion: true,
83
+ latestReleasePublishedAt: true,
84
+ latestReleaseCheckedAt: true,
75
85
  },
76
86
  props: {
77
87
  isRoot: true,
@@ -106,6 +116,12 @@ export default class GlobalConfigAPI extends BaseAPI<
106
116
  ? config?.enterpriseLicenseToken || null
107
117
  : null,
108
118
  licenseValid: licenseValid,
119
+ /*
120
+ * Whether this is an evaluation/testing license. Benign like the
121
+ * company name and expiry, so it is not gated behind sign-in — the
122
+ * edition modal shows the evaluation notice to anyone who opens it.
123
+ */
124
+ isEvaluationLicense: Boolean(config?.enterpriseLicenseIsEvaluation),
109
125
  userLimit:
110
126
  typeof config?.enterpriseLicenseUserLimit === "number"
111
127
  ? config.enterpriseLicenseUserLimit
@@ -126,6 +142,38 @@ export default class GlobalConfigAPI extends BaseAPI<
126
142
  isAuthenticatedUser && config?.instanceId
127
143
  ? config.instanceId.toString()
128
144
  : null,
145
+ /*
146
+ * Which build this installation runs, and whether a newer one has
147
+ * been released, are gated the same way as the instance topology:
148
+ * telling an anonymous visitor on the login page that this server
149
+ * is behind on patches advertises an unpatched target.
150
+ */
151
+ currentVersion: isAuthenticatedUser ? AppVersion : null,
152
+ latestVersion: isAuthenticatedUser
153
+ ? config?.latestReleaseVersion || null
154
+ : null,
155
+ latestVersionPublishedAt:
156
+ isAuthenticatedUser && config?.latestReleasePublishedAt
157
+ ? config.latestReleasePublishedAt.toISOString()
158
+ : null,
159
+ latestVersionCheckedAt:
160
+ isAuthenticatedUser && config?.latestReleaseCheckedAt
161
+ ? config.latestReleaseCheckedAt.toISOString()
162
+ : null,
163
+ isUpdateAvailable: isAuthenticatedUser
164
+ ? VersionUtil.isUpdateAvailable({
165
+ currentVersion: AppVersion,
166
+ latestVersion: config?.latestReleaseVersion,
167
+ })
168
+ : false,
169
+ /*
170
+ * Lets the modal say "update checks are off" instead of "has not
171
+ * checked yet", which would be a promise the installation is never
172
+ * going to keep.
173
+ */
174
+ isUpdateCheckDisabled: isAuthenticatedUser
175
+ ? DisableUpdateCheck
176
+ : false,
129
177
  };
130
178
 
131
179
  return Response.sendJsonObjectResponse(req, res, responseBody);
@@ -179,6 +227,12 @@ export default class GlobalConfigAPI extends BaseAPI<
179
227
  licenseKey,
180
228
  instanceId: instanceId.toString(),
181
229
  host: Host,
230
+ /*
231
+ * Sent here as well as from the daily report job so the version
232
+ * lands on the license server the moment the key is validated,
233
+ * rather than up to 24 hours later.
234
+ */
235
+ version: AppVersion,
182
236
  },
183
237
  });
184
238
 
@@ -239,6 +293,9 @@ export default class GlobalConfigAPI extends BaseAPI<
239
293
  }
240
294
  }
241
295
 
296
+ const isEvaluationLicense: boolean =
297
+ payload["isEvaluationLicense"] === true;
298
+
242
299
  const instances: Array<EnterpriseLicenseInstanceSummary> =
243
300
  Array.isArray(payload["instances"])
244
301
  ? (payload[
@@ -251,6 +308,7 @@ export default class GlobalConfigAPI extends BaseAPI<
251
308
  enterpriseLicenseKey: licenseKeyRaw || null,
252
309
  enterpriseLicenseExpiresAt: licenseExpiry || null,
253
310
  enterpriseLicenseToken: licenseToken || null,
311
+ enterpriseLicenseIsEvaluation: isEvaluationLicense,
254
312
  enterpriseLicenseUserLimit: userLimit,
255
313
  enterpriseLicenseCurrentUserCount: currentUserCount,
256
314
  enterpriseLicenseUserCountUpdatedAt: userCountUpdatedAt,
@@ -287,6 +345,8 @@ export default class GlobalConfigAPI extends BaseAPI<
287
345
  newConfig.enterpriseLicenseToken = licenseToken;
288
346
  }
289
347
 
348
+ newConfig.enterpriseLicenseIsEvaluation = isEvaluationLicense;
349
+
290
350
  if (licenseExpiry) {
291
351
  newConfig.enterpriseLicenseExpiresAt = licenseExpiry;
292
352
  }
@@ -321,6 +381,7 @@ export default class GlobalConfigAPI extends BaseAPI<
321
381
  expiresAt: licenseExpiry ? licenseExpiry.toISOString() : null,
322
382
  licenseKey: licenseKeyRaw || null,
323
383
  token: licenseToken || null,
384
+ isEvaluationLicense: isEvaluationLicense,
324
385
  userLimit: userLimit,
325
386
  currentUserCount: currentUserCount,
326
387
  userCountUpdatedAt: userCountUpdatedAt
@@ -665,6 +665,14 @@ export const IpWhitelist: string = process.env["IP_WHITELIST"] || "";
665
665
  export const DisableTelemetry: boolean =
666
666
  process.env["DISABLE_TELEMETRY"] === "true";
667
667
 
668
+ /*
669
+ * Opt out of the daily "is a newer OneUptime released?" check against the
670
+ * GitHub API. Deliberately separate from DISABLE_TELEMETRY, which turns off
671
+ * the OpenTelemetry SDK and says nothing about outbound calls.
672
+ */
673
+ export const DisableUpdateCheck: boolean =
674
+ process.env["DISABLE_UPDATE_CHECK"] === "true";
675
+
668
676
  export const EnableProfiling: boolean =
669
677
  process.env["ENABLE_PROFILING"] === "true";
670
678
 
@@ -782,6 +790,20 @@ export const EnterpriseLicenseUserCountReportUrl: URL = URL.fromString(
782
790
  "https://oneuptime.com/api/enterprise-license/report-user-count",
783
791
  );
784
792
 
793
+ /*
794
+ * GitHub's "latest release" endpoint. It already excludes drafts and
795
+ * prereleases, so whatever it returns is a version an administrator can
796
+ * safely be told to upgrade to.
797
+ *
798
+ * Overridable so an installation with no route to github.com can point the
799
+ * check at an internal mirror instead of turning it off entirely. The mirror
800
+ * must answer with GitHub's release shape (tag_name, html_url, published_at).
801
+ */
802
+ export const LatestReleaseCheckUrl: URL = URL.fromString(
803
+ process.env["LATEST_RELEASE_CHECK_URL"] ||
804
+ "https://api.github.com/repos/OneUptime/oneuptime/releases/latest",
805
+ );
806
+
785
807
  // Inbound Email Configuration for Incoming Email Monitor
786
808
  export enum InboundEmailProviderType {
787
809
  SendGrid = "SendGrid",
@@ -0,0 +1,38 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddInstanceVersionAndLatestReleaseColumns1784659816363
4
+ implements MigrationInterface
5
+ {
6
+ public name: string =
7
+ "AddInstanceVersionAndLatestReleaseColumns1784659816363";
8
+
9
+ public async up(queryRunner: QueryRunner): Promise<void> {
10
+ await queryRunner.query(
11
+ `ALTER TABLE "EnterpriseLicenseInstance" ADD "oneuptimeVersion" character varying(100)`,
12
+ );
13
+ await queryRunner.query(
14
+ `ALTER TABLE "GlobalConfig" ADD "latestReleaseVersion" character varying(100)`,
15
+ );
16
+ await queryRunner.query(
17
+ `ALTER TABLE "GlobalConfig" ADD "latestReleasePublishedAt" TIMESTAMP WITH TIME ZONE`,
18
+ );
19
+ await queryRunner.query(
20
+ `ALTER TABLE "GlobalConfig" ADD "latestReleaseCheckedAt" TIMESTAMP WITH TIME ZONE`,
21
+ );
22
+ }
23
+
24
+ public async down(queryRunner: QueryRunner): Promise<void> {
25
+ await queryRunner.query(
26
+ `ALTER TABLE "GlobalConfig" DROP COLUMN "latestReleaseCheckedAt"`,
27
+ );
28
+ await queryRunner.query(
29
+ `ALTER TABLE "GlobalConfig" DROP COLUMN "latestReleasePublishedAt"`,
30
+ );
31
+ await queryRunner.query(
32
+ `ALTER TABLE "GlobalConfig" DROP COLUMN "latestReleaseVersion"`,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "EnterpriseLicenseInstance" DROP COLUMN "oneuptimeVersion"`,
36
+ );
37
+ }
38
+ }
@@ -0,0 +1,25 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddEnterpriseLicenseEvaluationColumns1784705487674
4
+ implements MigrationInterface
5
+ {
6
+ public name: string = "AddEnterpriseLicenseEvaluationColumns1784705487674";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `ALTER TABLE "EnterpriseLicense" ADD "isEvaluationLicense" boolean NOT NULL DEFAULT false`,
11
+ );
12
+ await queryRunner.query(
13
+ `ALTER TABLE "GlobalConfig" ADD "enterpriseLicenseIsEvaluation" boolean DEFAULT false`,
14
+ );
15
+ }
16
+
17
+ public async down(queryRunner: QueryRunner): Promise<void> {
18
+ await queryRunner.query(
19
+ `ALTER TABLE "GlobalConfig" DROP COLUMN "enterpriseLicenseIsEvaluation"`,
20
+ );
21
+ await queryRunner.query(
22
+ `ALTER TABLE "EnterpriseLicense" DROP COLUMN "isEvaluationLicense"`,
23
+ );
24
+ }
25
+ }
@@ -462,6 +462,8 @@ import { AddEnterpriseLicenseNotificationColumns1784218257664 } from "./17842182
462
462
  import { AddAttributionColumnsToUserAndProject1784293516000 } from "./1784293516000-AddAttributionColumnsToUserAndProject";
463
463
  import { AddMarketingConversionTable1784298000000 } from "./1784298000000-AddMarketingConversionTable";
464
464
  import { AddExceptionTriageAndPrivacyColumns1784640000000 } from "./1784640000000-AddExceptionTriageAndPrivacyColumns";
465
+ import { AddInstanceVersionAndLatestReleaseColumns1784659816363 } from "./1784659816363-AddInstanceVersionAndLatestReleaseColumns";
466
+ import { AddEnterpriseLicenseEvaluationColumns1784705487674 } from "./1784705487674-AddEnterpriseLicenseEvaluationColumns";
465
467
 
466
468
  export default [
467
469
  InitialMigration,
@@ -928,4 +930,6 @@ export default [
928
930
  AddAttributionColumnsToUserAndProject1784293516000,
929
931
  AddMarketingConversionTable1784298000000,
930
932
  AddExceptionTriageAndPrivacyColumns1784640000000,
933
+ AddInstanceVersionAndLatestReleaseColumns1784659816363,
934
+ AddEnterpriseLicenseEvaluationColumns1784705487674,
931
935
  ];
@@ -47,7 +47,7 @@ import {
47
47
  ResponseJSON,
48
48
  ResultSet,
49
49
  } from "@clickhouse/client";
50
- import { AsyncLocalStorage } from "node:async_hooks";
50
+ import { nextInsertDedupToken } from "../Utils/AnalyticsDatabase/InsertDedupContext";
51
51
  import AnalyticsBaseModel from "../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
52
52
  import { WorkflowRoute } from "../../ServiceRoute";
53
53
  import Protocol from "../../Types/API/Protocol";
@@ -140,40 +140,42 @@ export const MigrationExecuteOptions: ClickhouseExecuteOptions = {
140
140
  },
141
141
  };
142
142
 
143
- /**
144
- * Ambient context that makes ClickHouse inserts idempotent across queue
145
- * retries. The telemetry queue worker wraps each job in
146
- * `runWithInsertDedup(jobId, ...)`; every insertJsonRows call inside the
147
- * job then stamps `insert_deduplication_token =
148
- * "<tokenBase>:<table>:<chunkIndex>"` plus async_insert_deduplicate=1 /
149
- * wait_for_async_insert=1, so a stalled-job retry that re-processes the
150
- * same payload re-issues byte-identical tokens and ClickHouse drops the
151
- * duplicate blocks (on replicated tables; on plain MergeTree the token is
152
- * ignored unless non_replicated_deduplication_window is set — no harm
153
- * either way). The chunk counter is per table because one job inserts
154
- * into several tables (e.g. Span + ExceptionInstance) in a deterministic
155
- * order.
143
+ /*
144
+ * The insert-dedup ambient context lives in
145
+ * Utils/AnalyticsDatabase/InsertDedupContext so that both this service and
146
+ * TelemetryFanInWriter can consume deterministic tokens without an import
147
+ * cycle. Re-exported here for existing callers.
156
148
  *
157
- * HTTP-path inserts run outside the context and keep the fire-and-forget
158
- * async insert (wait_for_async_insert=0) — dedup waiting is only
159
- * affordable off the request thread.
149
+ * HTTP-path inserts run outside the context and are always fire-and-forget
150
+ * (wait_for_async_insert=0) — flush waiting, when opted into via
151
+ * TELEMETRY_WAIT_FOR_ASYNC_INSERT, is only affordable off the request
152
+ * thread and therefore only applies to tokened (queue-job) inserts.
160
153
  */
161
- export interface InsertDedupContextStore {
162
- tokenBase: string;
163
- chunkIndexByTable: Map<string, number>;
164
- }
154
+ export {
155
+ runWithInsertDedup,
156
+ nextInsertDedupToken,
157
+ type InsertDedupContextStore,
158
+ } from "../Utils/AnalyticsDatabase/InsertDedupContext";
165
159
 
166
- const insertDedupContext: AsyncLocalStorage<InsertDedupContextStore> =
167
- new AsyncLocalStorage<InsertDedupContextStore>();
168
-
169
- export function runWithInsertDedup<T>(
170
- tokenBase: string,
171
- fn: () => Promise<T>,
172
- ): Promise<T> {
173
- return insertDedupContext.run(
174
- { tokenBase, chunkIndexByTable: new Map<string, number>() },
175
- fn,
176
- );
160
+ /*
161
+ * Ack mode for telemetry ClickHouse inserts.
162
+ *
163
+ * Default (false): wait_for_async_insert=0 — fire-and-forget. ClickHouse
164
+ * acks as soon as the batch is accepted into its async-insert buffer and
165
+ * owns flushing from there; inserts release their query slot almost
166
+ * immediately. The trade: flush-time errors surface only in server logs
167
+ * (system.asynchronous_inserts / part log), and a ClickHouse crash between
168
+ * buffer-accept and flush loses that buffer even though callers were acked.
169
+ *
170
+ * Set TELEMETRY_WAIT_FOR_ASYNC_INSERT=true to make every tokened insert
171
+ * wait for the durable flush before acking (ack-after-flush end to end
172
+ * through the fan-in writer and writer tier) — at the cost of each waiting
173
+ * insert holding a ClickHouse query slot until its buffer flushes.
174
+ */
175
+ export function shouldWaitForAsyncInsert(): boolean {
176
+ const raw: string | undefined =
177
+ process.env["TELEMETRY_WAIT_FOR_ASYNC_INSERT"];
178
+ return raw === "true" || raw === "1";
177
179
  }
178
180
 
179
181
  export default class AnalyticsDatabaseService<
@@ -259,30 +261,31 @@ export default class AnalyticsDatabaseService<
259
261
  let dedupToken: string | undefined = options?.dedupToken;
260
262
 
261
263
  if (!dedupToken) {
262
- const dedupStore: InsertDedupContextStore | undefined =
263
- insertDedupContext.getStore();
264
- if (dedupStore) {
265
- const chunkIndex: number =
266
- dedupStore.chunkIndexByTable.get(tableName) ?? 0;
267
- dedupStore.chunkIndexByTable.set(tableName, chunkIndex + 1);
268
- dedupToken = `${dedupStore.tokenBase}:${tableName}:${chunkIndex}`;
269
- }
264
+ dedupToken = nextInsertDedupToken(tableName);
270
265
  }
271
266
 
267
+ const waitForAsyncInsert: 0 | 1 = shouldWaitForAsyncInsert() ? 1 : 0;
268
+
272
269
  let clickhouseSettings: ClickHouseSettings = {
273
270
  async_insert: 1,
274
- wait_for_async_insert: 0,
271
+ wait_for_async_insert: waitForAsyncInsert,
275
272
  };
276
273
 
277
274
  if (dedupToken) {
278
275
  /*
279
- * wait_for_async_insert=1 so the worker only acks the job after
280
- * the block actually landed (or was deduplicated) otherwise a
281
- * crash between buffer-write and flush loses data with no retry.
276
+ * Dedup settings ride along in both ack modes. For async inserts
277
+ * ClickHouse dedups by content hash of the insert body when
278
+ * async_insert_deduplicate=1 (the explicit token is not yet honored
279
+ * for async inserts — ClickHouse #52018), so byte-identical queue
280
+ * retries are still dropped. The ack mode is a separate, deliberate
281
+ * trade (see shouldWaitForAsyncInsert): by default ClickHouse owns
282
+ * flushing and an ack means "accepted into the async-insert buffer";
283
+ * with TELEMETRY_WAIT_FOR_ASYNC_INSERT=true the ack waits for the
284
+ * durable flush instead.
282
285
  */
283
286
  clickhouseSettings = {
284
287
  async_insert: 1,
285
- wait_for_async_insert: 1,
288
+ wait_for_async_insert: waitForAsyncInsert,
286
289
  async_insert_deduplicate: 1,
287
290
  insert_deduplication_token: dedupToken,
288
291
  };
@@ -980,7 +983,19 @@ export default class AnalyticsDatabaseService<
980
983
  onBeforeFind.select = {} as any;
981
984
  }
982
985
 
983
- if (!(onBeforeFind.select as any)["_id"]) {
986
+ /*
987
+ * Derived aggregate targets deliberately omit AnalyticsBaseModel's
988
+ * synthetic `_id`: the aggregation key is the row identity. Only force
989
+ * `_id` into generic reads when the model actually declares it.
990
+ */
991
+ if (
992
+ this.model.tableColumns.some(
993
+ (column: AnalyticsTableColumn): boolean => {
994
+ return column.key === "_id";
995
+ },
996
+ ) &&
997
+ !(onBeforeFind.select as any)["_id"]
998
+ ) {
984
999
  (onBeforeFind.select as any)["_id"] = true;
985
1000
  }
986
1001
 
@@ -1743,10 +1743,67 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1743
1743
  });
1744
1744
  }
1745
1745
 
1746
+ /*
1747
+ * Update `data` may arrive as a full model instance rather than a plain
1748
+ * partial — the QueryDeepPartialEntity type structurally admits both, and
1749
+ * callers do construct `new Model()` payloads. A model instance carries
1750
+ * non-column own properties from DatabaseBaseModel (every column
1751
+ * initializer plus `isPermissionIf = {}`), and _updateBy turns every data
1752
+ * key into a select column for its internal find, so the extra keys made
1753
+ * that find throw `TableColumnMetadata not found for isPermissionIf
1754
+ * column` and fail the whole update. Strip a model instance down to its
1755
+ * set table columns; plain objects pass through untouched so a typo'd
1756
+ * column name in a literal still fails loudly instead of being silently
1757
+ * dropped.
1758
+ *
1759
+ * `_id`, `createdAt`, `updatedAt` and `version` are dropped for model
1760
+ * instances the same way BaseAPI drops them from client payloads: the row
1761
+ * is located by the update query (spreading a foreign `_id` into the save
1762
+ * payload would redirect the write), timestamps are database-managed, and
1763
+ * `version` is TypeORM's optimistic-concurrency counter.
1764
+ */
1765
+ public sanitizeUpdateData(
1766
+ data: UpdateBy<TBaseModel>["data"],
1767
+ ): UpdateBy<TBaseModel>["data"] {
1768
+ if (!(data instanceof BaseModel)) {
1769
+ return data;
1770
+ }
1771
+
1772
+ const plainData: JSONObject = {};
1773
+
1774
+ for (const key of Object.keys(data)) {
1775
+ if (!this.model.isTableColumn(key)) {
1776
+ continue;
1777
+ }
1778
+
1779
+ if (
1780
+ key === "_id" ||
1781
+ key === "createdAt" ||
1782
+ key === "updatedAt" ||
1783
+ key === "version"
1784
+ ) {
1785
+ continue;
1786
+ }
1787
+
1788
+ const value: JSONValue = (data as any)[key];
1789
+
1790
+ // Unset columns must not become writes (or select columns).
1791
+ if (value === undefined) {
1792
+ continue;
1793
+ }
1794
+
1795
+ plainData[key] = value;
1796
+ }
1797
+
1798
+ return plainData as UpdateBy<TBaseModel>["data"];
1799
+ }
1800
+
1746
1801
  private async _updateBy(updateBy: UpdateBy<TBaseModel>): Promise<number> {
1747
1802
  try {
1748
1803
  this.setTelemetryContextFromProps(updateBy.props);
1749
1804
 
1805
+ updateBy.data = this.sanitizeUpdateData(updateBy.data);
1806
+
1750
1807
  const onUpdate: OnUpdate<TBaseModel> = updateBy.props.ignoreHooks
1751
1808
  ? { updateBy, carryForward: [] }
1752
1809
  : await this.onBeforeUpdate(updateBy);
@@ -1830,9 +1887,17 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1830
1887
  });
1831
1888
 
1832
1889
  for (const item of items) {
1890
+ /*
1891
+ * _id must be set AFTER the spread: update data can carry an
1892
+ * explicit `_id: undefined` (sanitizeUpdateData strips it from model
1893
+ * instances, but a plain object can still hold it), and spreading it
1894
+ * after _id would clobber the located row's id — save() then sees no
1895
+ * primary key, INSERTs instead of updating, and dies on the first
1896
+ * NOT NULL column.
1897
+ */
1833
1898
  const updatedItem: any = {
1834
- _id: item._id!,
1835
1899
  ...data,
1900
+ _id: item._id!,
1836
1901
  } as any;
1837
1902
 
1838
1903
  logger.debug("Updated Item", {
@@ -49,6 +49,40 @@ export class Service extends DatabaseService<Model> {
49
49
  createdItem.webhookSecretKey = secretKey;
50
50
  }
51
51
 
52
+ /*
53
+ * A workflow that arrives with its graph already in place - imported from
54
+ * a JSON export, or duplicated from another workflow - never passes
55
+ * through onUpdateSuccess, so nothing has denormalized its trigger onto
56
+ * the row. The runner looks workflows up by triggerId, so without this the
57
+ * workflow is saved and looks correct in the builder but silently never
58
+ * fires.
59
+ */
60
+ if (createdItem.id && createdItem.graph) {
61
+ await this.saveTriggerFromGraph({
62
+ workflowId: createdItem.id,
63
+ graph: createdItem.graph,
64
+ });
65
+
66
+ /*
67
+ * Registers schedule triggers with the runner. Best effort: the row and
68
+ * its trigger are already persisted, so a workflow service outage must
69
+ * not fail the create. The trigger is already on the row, so the next
70
+ * save - or the runner's own startup scan in Schedule.init, which
71
+ * queries by triggerId - picks the workflow up.
72
+ */
73
+ try {
74
+ await this.notifyWorkflowService(createdItem.id);
75
+ } catch (error) {
76
+ logger.error(
77
+ `Error notifying workflow service of created workflow: ${error}`,
78
+ {
79
+ projectId: createdItem.projectId?.toString(),
80
+ workflowId: createdItem.id?.toString(),
81
+ } as LogAttributes,
82
+ );
83
+ }
84
+ }
85
+
52
86
  if (createdItem.projectId && createdItem.id) {
53
87
  /*
54
88
  * Run label rule first so rule-added labels are persisted before
@@ -87,39 +121,13 @@ export class Service extends DatabaseService<Model> {
87
121
  ): Promise<OnUpdate<Model>> {
88
122
  /// save trigger and trigger args.
89
123
 
90
- if (
91
- onUpdate.updateBy.data &&
92
- (onUpdate.updateBy.data as any).graph &&
93
- (((onUpdate.updateBy.data as any).graph as any)[
94
- "nodes"
95
- ] as Array<JSONObject>)
96
- ) {
97
- let trigger: NodeDataProp | null = null;
98
-
99
- // check if it has a trigger node.
100
- for (const node of ((onUpdate.updateBy.data as any).graph as any)[
101
- "nodes"
102
- ] as Array<JSONObject>) {
103
- const nodeData: NodeDataProp = node["data"] as any;
104
- if (
105
- nodeData.componentType === ComponentType.Trigger &&
106
- nodeData.nodeType === NodeType.Node
107
- ) {
108
- // found the trigger;
109
- trigger = nodeData;
110
- }
111
- }
124
+ const updatedGraph: JSONObject | undefined = (onUpdate.updateBy.data as any)
125
+ ?.graph as JSONObject | undefined;
112
126
 
113
- await this.updateOneById({
114
- id: new ObjectID(onUpdate.updateBy.query._id! as any),
115
- data: {
116
- triggerId: trigger?.metadataId || null,
117
- triggerArguments: trigger?.arguments || {},
118
- } as any,
119
- props: {
120
- isRoot: true,
121
- ignoreHooks: true,
122
- },
127
+ if (updatedGraph) {
128
+ await this.saveTriggerFromGraph({
129
+ workflowId: new ObjectID(onUpdate.updateBy.query._id! as any),
130
+ graph: updatedGraph,
123
131
  });
124
132
  }
125
133
 
@@ -127,23 +135,76 @@ export class Service extends DatabaseService<Model> {
127
135
  workflowId: onUpdate.updateBy.query._id?.toString(),
128
136
  } as LogAttributes);
129
137
 
138
+ await this.notifyWorkflowService(
139
+ new ObjectID(onUpdate.updateBy.query._id! as any),
140
+ );
141
+
142
+ logger.debug("Updated workflow on the workflow service", {
143
+ workflowId: onUpdate.updateBy.query._id?.toString(),
144
+ } as LogAttributes);
145
+
146
+ return onUpdate;
147
+ }
148
+
149
+ /*
150
+ * The trigger node is denormalized out of the graph onto triggerId /
151
+ * triggerArguments because the runner queries workflows by trigger and
152
+ * cannot parse every graph to do it. A graph with no trigger node clears
153
+ * both columns, which is how a workflow stops firing when its trigger is
154
+ * removed in the builder.
155
+ */
156
+ private async saveTriggerFromGraph(data: {
157
+ workflowId: ObjectID;
158
+ graph: JSONObject;
159
+ }): Promise<void> {
160
+ const nodes: Array<JSONObject> | undefined = data.graph["nodes"] as
161
+ | Array<JSONObject>
162
+ | undefined;
163
+
164
+ if (!nodes || !Array.isArray(nodes)) {
165
+ return;
166
+ }
167
+
168
+ let trigger: NodeDataProp | null = null;
169
+
170
+ // check if it has a trigger node.
171
+ for (const node of nodes) {
172
+ const nodeData: NodeDataProp = node["data"] as any;
173
+
174
+ if (
175
+ nodeData?.componentType === ComponentType.Trigger &&
176
+ nodeData?.nodeType === NodeType.Node
177
+ ) {
178
+ // found the trigger;
179
+ trigger = nodeData;
180
+ }
181
+ }
182
+
183
+ await this.updateOneById({
184
+ id: data.workflowId,
185
+ data: {
186
+ triggerId: trigger?.metadataId || null,
187
+ triggerArguments: trigger?.arguments || {},
188
+ } as any,
189
+ props: {
190
+ isRoot: true,
191
+ ignoreHooks: true,
192
+ },
193
+ });
194
+ }
195
+
196
+ private async notifyWorkflowService(workflowId: ObjectID): Promise<void> {
130
197
  await API.post<EmptyResponseData>({
131
198
  url: new URL(
132
199
  Protocol.HTTP,
133
200
  WorkflowHostname,
134
- new Route("/workflow/update/" + onUpdate.updateBy.query._id!),
201
+ new Route("/workflow/update/" + workflowId.toString()),
135
202
  ),
136
203
  data: {},
137
204
  headers: {
138
205
  ...ClusterKeyAuthorization.getClusterKeyHeaders(),
139
206
  },
140
207
  });
141
-
142
- logger.debug("Updated workflow on the workflow service", {
143
- workflowId: onUpdate.updateBy.query._id?.toString(),
144
- } as LogAttributes);
145
-
146
- return onUpdate;
147
208
  }
148
209
  }
149
210
  export default new Service();