@oneuptime/common 11.5.12 → 11.6.0

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 (193) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +30 -0
  2. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  3. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  4. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  5. package/Models/DatabaseModels/Project.ts +30 -0
  6. package/Models/DatabaseModels/TelemetryException.ts +128 -0
  7. package/Server/API/AIAgentDataAPI.ts +129 -5
  8. package/Server/API/AIInvestigationAPI.ts +87 -0
  9. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  10. package/Server/API/GlobalConfigAPI.ts +62 -1
  11. package/Server/EnvironmentConfig.ts +22 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIRunService.ts +8 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +48 -45
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/TelemetryExceptionService.ts +320 -24
  20. package/Server/Services/WorkflowService.ts +100 -39
  21. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  22. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
  23. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
  24. package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
  25. package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
  26. package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
  27. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
  28. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  29. package/Server/Utils/Express.ts +13 -0
  30. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
  31. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  32. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  33. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  34. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  35. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  36. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
  37. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  38. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  39. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
  40. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
  41. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  42. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
  43. package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
  44. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  45. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  46. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  47. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  48. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  49. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  50. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  51. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  52. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  53. package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
  54. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  55. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  56. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  57. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  58. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  59. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  60. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  61. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  62. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  63. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  64. package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
  65. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  66. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  67. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  68. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  69. package/Tests/Utils/Array.test.ts +156 -0
  70. package/Tests/Utils/CronTab.test.ts +199 -0
  71. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  72. package/Tests/Utils/Number.test.ts +179 -0
  73. package/Tests/Utils/VersionUtil.test.ts +348 -0
  74. package/Tests/jest.setup.ts +1 -0
  75. package/Types/AI/CodeFixTaskContext.ts +6 -0
  76. package/Types/AI/CodeFixTaskType.ts +38 -1
  77. package/Types/AI/ExceptionAIClassification.ts +27 -0
  78. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  79. package/Types/Log/LogScrubPatternType.ts +7 -0
  80. package/Types/Monitor/MonitorStep.ts +24 -4
  81. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  82. package/Types/Trace/TraceScrubPatternType.ts +7 -0
  83. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  84. package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
  85. package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
  86. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  87. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  88. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  89. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  90. package/Utils/CronTab.ts +823 -0
  91. package/Utils/ModelImportExport.ts +21 -6
  92. package/Utils/VersionUtil.ts +260 -0
  93. package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
  94. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  100. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/Project.js +31 -0
  102. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
  104. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  105. package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
  106. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  107. package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
  108. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  109. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  110. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  111. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  112. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  113. package/build/dist/Server/EnvironmentConfig.js +17 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AIRunService.js +9 -2
  124. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  125. package/build/dist/Server/Services/AnalyticsDatabaseService.js +44 -16
  126. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  127. package/build/dist/Server/Services/DatabaseService.js +53 -1
  128. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  129. package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
  130. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  131. package/build/dist/Server/Services/WorkflowService.js +80 -30
  132. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  133. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  134. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  135. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
  136. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  137. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
  138. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  139. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
  140. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
  141. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
  142. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
  143. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
  144. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
  145. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
  146. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
  147. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  148. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  149. package/build/dist/Server/Utils/Express.js +12 -0
  150. package/build/dist/Server/Utils/Express.js.map +1 -1
  151. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
  152. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
  153. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  154. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  155. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  156. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  157. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  158. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  159. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  160. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  161. package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
  162. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  163. package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
  164. package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
  165. package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
  166. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
  167. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  168. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  169. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  170. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  171. package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
  172. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
  173. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  174. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  175. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
  176. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  177. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
  178. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
  179. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  180. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  181. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  182. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  183. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  184. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  185. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  186. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  187. package/build/dist/Utils/CronTab.js +609 -0
  188. package/build/dist/Utils/CronTab.js.map +1 -0
  189. package/build/dist/Utils/ModelImportExport.js +20 -6
  190. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  191. package/build/dist/Utils/VersionUtil.js +193 -0
  192. package/build/dist/Utils/VersionUtil.js.map +1 -0
  193. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { JSONObject } from "../../Types/JSON";
5
5
  import PartialEntity from "../../Types/Database/PartialEntity";
6
6
  import EnterpriseLicenseInstanceSummary from "../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
7
7
  import EnterpriseLicenseUsageUtil from "../../Utils/EnterpriseLicense/EnterpriseLicenseUsage";
8
+ import VersionUtil from "../../Utils/VersionUtil";
8
9
  import LIMIT_MAX from "../../Types/Database/LimitMax";
9
10
  import ObjectID from "../../Types/ObjectID";
10
11
  import PositiveNumber from "../../Types/PositiveNumber";
@@ -29,6 +30,8 @@ export interface LicenseInstanceUpsert {
29
30
  licenseId: ObjectID;
30
31
  instanceId: string;
31
32
  host: string | undefined;
33
+ // OneUptime version the instance is running. Absent on instances too old to report it.
34
+ oneuptimeVersion?: string | null | undefined;
32
35
  // Usage fields are only set on report-user-count, not on validate.
33
36
  userCount?: number | undefined;
34
37
  userEmailHashes?: Array<string> | undefined;
@@ -84,6 +87,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
84
87
  userLimit: true,
85
88
  currentUserCount: true,
86
89
  userCountUpdatedAt: true,
90
+ isEvaluationLicense: true,
87
91
  },
88
92
  props: {
89
93
  isRoot: true,
@@ -117,12 +121,16 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
117
121
  req.body["instanceId"],
118
122
  );
119
123
  const instanceHost: string = this.parseShortText(req.body["host"]);
124
+ const instanceVersion: string | null | undefined = this.parseVersion(
125
+ req.body["version"],
126
+ );
120
127
 
121
128
  if (instanceId) {
122
129
  await this.upsertLicenseInstance({
123
130
  licenseId: license.id!,
124
131
  instanceId: instanceId,
125
132
  host: instanceHost || undefined,
133
+ oneuptimeVersion: instanceVersion,
126
134
  });
127
135
  }
128
136
 
@@ -154,6 +162,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
154
162
  userCountUpdatedAt: license.userCountUpdatedAt
155
163
  ? license.userCountUpdatedAt.toISOString()
156
164
  : null,
165
+ isEvaluationLicense: Boolean(license.isEvaluationLicense),
157
166
  instances: this.getInstanceSummaries(instances),
158
167
  token,
159
168
  });
@@ -196,6 +205,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
196
205
  select: {
197
206
  _id: true,
198
207
  userLimit: true,
208
+ isEvaluationLicense: true,
199
209
  },
200
210
  props: {
201
211
  isRoot: true,
@@ -212,6 +222,9 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
212
222
  req.body["instanceId"],
213
223
  );
214
224
  const instanceHost: string = this.parseShortText(req.body["host"]);
225
+ const instanceVersion: string | null | undefined = this.parseVersion(
226
+ req.body["version"],
227
+ );
215
228
  const userEmailHashes: Array<string> =
216
229
  EnterpriseLicenseUsageUtil.sanitizeUserEmailHashes(
217
230
  req.body["userEmailHashes"],
@@ -230,6 +243,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
230
243
  licenseId: license.id!,
231
244
  instanceId: instanceId,
232
245
  host: instanceHost || undefined,
246
+ oneuptimeVersion: instanceVersion,
233
247
  userCount: userCount,
234
248
  userEmailHashes: userEmailHashes,
235
249
  masterAdminEmails: masterAdminEmails,
@@ -273,6 +287,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
273
287
  userCountUpdatedAt: reportedAt.toISOString(),
274
288
  userLimit:
275
289
  typeof license.userLimit === "number" ? license.userLimit : null,
290
+ isEvaluationLicense: Boolean(license.isEvaluationLicense),
276
291
  instances: this.getInstanceSummaries(instances),
277
292
  });
278
293
  } catch (err) {
@@ -290,6 +305,31 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
290
305
  return value.trim().substring(0, 100);
291
306
  }
292
307
 
308
+ /*
309
+ * report-user-count is unauthenticated, so the reported version is only
310
+ * stored when it is a version we could actually compare against a release —
311
+ * garbage never reaches the admin dashboard or the customer's modal.
312
+ *
313
+ * Three outcomes, and the callers depend on the distinction:
314
+ * undefined — the instance sent no version at all (it predates this
315
+ * field). Leave whatever is stored alone.
316
+ * null — it sent something, but not a version. Clear the stored
317
+ * value, so an instance rolled back to a build with no
318
+ * APP_VERSION stops advertising the version it used to run.
319
+ * string — a valid, canonical version to store.
320
+ */
321
+ private parseVersion(value: unknown): string | null | undefined {
322
+ if (value === undefined || value === null) {
323
+ return undefined;
324
+ }
325
+
326
+ /*
327
+ * Canonicalized rather than passed through, so the stored string is never
328
+ * something the UIs would render with a doubled "v".
329
+ */
330
+ return VersionUtil.canonicalize(value);
331
+ }
332
+
293
333
  private async findLicenseInstances(
294
334
  licenseId: ObjectID,
295
335
  ): Promise<Array<EnterpriseLicenseInstance>> {
@@ -304,6 +344,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
304
344
  userCount: true,
305
345
  userEmailHashes: true,
306
346
  lastReportedAt: true,
347
+ oneuptimeVersion: true,
307
348
  },
308
349
  sort: {
309
350
  createdAt: SortOrder.Ascending,
@@ -331,6 +372,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
331
372
  lastReportedAt: instance.lastReportedAt
332
373
  ? instance.lastReportedAt.toISOString()
333
374
  : null,
375
+ version: instance.oneuptimeVersion || null,
334
376
  };
335
377
  },
336
378
  );
@@ -370,6 +412,11 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
370
412
  newInstance.host = data.host;
371
413
  }
372
414
 
415
+ // Null means "reported something that is not a version" — nothing to store.
416
+ if (data.oneuptimeVersion) {
417
+ newInstance.oneuptimeVersion = data.oneuptimeVersion;
418
+ }
419
+
373
420
  if (data.userCount !== undefined) {
374
421
  newInstance.userCount = data.userCount;
375
422
  }
@@ -434,6 +481,16 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
434
481
  updateData.host = data.host;
435
482
  }
436
483
 
484
+ /*
485
+ * Null clears the column: an instance rebuilt onto a build with no
486
+ * APP_VERSION must stop advertising the version it used to run, because
487
+ * lastReportedAt is refreshed on the same request and would otherwise
488
+ * read as "confirmed running v11.5.13 as of today".
489
+ */
490
+ if (data.oneuptimeVersion !== undefined) {
491
+ updateData.oneuptimeVersion = data.oneuptimeVersion;
492
+ }
493
+
437
494
  if (data.userCount !== undefined) {
438
495
  updateData.userCount = data.userCount;
439
496
  }
@@ -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,56 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ /*
4
+ * AI exception triage + privacy columns:
5
+ *
6
+ * - TelemetryException.unhandled: sticky rollup of OTel exception.escaped
7
+ * (maintained by the ingest upsert with OR semantics).
8
+ * - TelemetryException.aiClassification: triage verdict (code-fault,
9
+ * user-error, expected-denial, infrastructure, unknown).
10
+ * - TelemetryException.aiFixDeclinedAt: stamped when a human closes an
11
+ * AI fix PR without merging; suppresses further automatic fix attempts.
12
+ * - AIInsight.classification: the same triage verdict on the insight row.
13
+ * - Project.autoArchiveNonActionableExceptions: opt-in auto-archive of
14
+ * expected-denial exception groups.
15
+ */
16
+ export class AddExceptionTriageAndPrivacyColumns1784640000000
17
+ implements MigrationInterface
18
+ {
19
+ public name: string = "AddExceptionTriageAndPrivacyColumns1784640000000";
20
+
21
+ public async up(queryRunner: QueryRunner): Promise<void> {
22
+ await queryRunner.query(
23
+ `ALTER TABLE "TelemetryException" ADD "unhandled" boolean NOT NULL DEFAULT false`,
24
+ );
25
+ await queryRunner.query(
26
+ `ALTER TABLE "TelemetryException" ADD "aiClassification" character varying(100)`,
27
+ );
28
+ await queryRunner.query(
29
+ `ALTER TABLE "TelemetryException" ADD "aiFixDeclinedAt" TIMESTAMP WITH TIME ZONE`,
30
+ );
31
+ await queryRunner.query(
32
+ `ALTER TABLE "AIInsight" ADD "classification" character varying(100)`,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "Project" ADD "autoArchiveNonActionableExceptions" boolean NOT NULL DEFAULT false`,
36
+ );
37
+ }
38
+
39
+ public async down(queryRunner: QueryRunner): Promise<void> {
40
+ await queryRunner.query(
41
+ `ALTER TABLE "Project" DROP COLUMN "autoArchiveNonActionableExceptions"`,
42
+ );
43
+ await queryRunner.query(
44
+ `ALTER TABLE "AIInsight" DROP COLUMN "classification"`,
45
+ );
46
+ await queryRunner.query(
47
+ `ALTER TABLE "TelemetryException" DROP COLUMN "aiFixDeclinedAt"`,
48
+ );
49
+ await queryRunner.query(
50
+ `ALTER TABLE "TelemetryException" DROP COLUMN "aiClassification"`,
51
+ );
52
+ await queryRunner.query(
53
+ `ALTER TABLE "TelemetryException" DROP COLUMN "unhandled"`,
54
+ );
55
+ }
56
+ }
@@ -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
+ }
@@ -461,6 +461,9 @@ import { AddNetworkDeviceInventoryAndDiscoverySchedule1784211212164 } from "./17
461
461
  import { AddEnterpriseLicenseNotificationColumns1784218257664 } from "./1784218257664-AddEnterpriseLicenseNotificationColumns";
462
462
  import { AddAttributionColumnsToUserAndProject1784293516000 } from "./1784293516000-AddAttributionColumnsToUserAndProject";
463
463
  import { AddMarketingConversionTable1784298000000 } from "./1784298000000-AddMarketingConversionTable";
464
+ import { AddExceptionTriageAndPrivacyColumns1784640000000 } from "./1784640000000-AddExceptionTriageAndPrivacyColumns";
465
+ import { AddInstanceVersionAndLatestReleaseColumns1784659816363 } from "./1784659816363-AddInstanceVersionAndLatestReleaseColumns";
466
+ import { AddEnterpriseLicenseEvaluationColumns1784705487674 } from "./1784705487674-AddEnterpriseLicenseEvaluationColumns";
464
467
 
465
468
  export default [
466
469
  InitialMigration,
@@ -926,4 +929,7 @@ export default [
926
929
  AddEnterpriseLicenseNotificationColumns1784218257664,
927
930
  AddAttributionColumnsToUserAndProject1784293516000,
928
931
  AddMarketingConversionTable1784298000000,
932
+ AddExceptionTriageAndPrivacyColumns1784640000000,
933
+ AddInstanceVersionAndLatestReleaseColumns1784659816363,
934
+ AddEnterpriseLicenseEvaluationColumns1784705487674,
929
935
  ];
@@ -187,6 +187,14 @@ export class Service extends DatabaseService<Model> {
187
187
  run.triggeredByIncidentId || run.triggeredByAlertId
188
188
  ? null
189
189
  : "Queued code-fix run has no incident or alert subject.";
190
+ } else if (
191
+ run.codeFixTaskType === CodeFixTaskType.ImproveLogging ||
192
+ run.codeFixTaskType === CodeFixTaskType.ImproveTracing
193
+ ) {
194
+ // Service-scoped instrumentation recipes carry the service instead.
195
+ missingContextMessage = run.taskContext?.telemetryServiceId
196
+ ? null
197
+ : "Queued code-fix run has no telemetry service in its task context.";
190
198
  } else {
191
199
  missingContextMessage = run.taskContext?.traceId
192
200
  ? null
@@ -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
  };