@oneuptime/common 11.3.8 → 11.3.10

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 (80) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +27 -0
  2. package/Models/AnalyticsModels/Log.ts +1 -0
  3. package/Models/AnalyticsModels/Metric.ts +1 -0
  4. package/Models/AnalyticsModels/MutableMetric.ts +333 -0
  5. package/Models/AnalyticsModels/Profile.ts +1 -0
  6. package/Models/AnalyticsModels/Span.ts +1 -0
  7. package/Server/Infrastructure/Queue.ts +19 -2
  8. package/Server/Infrastructure/QueueWorker.ts +67 -1
  9. package/Server/Services/AlertService.ts +167 -206
  10. package/Server/Services/AlertStateTimelineService.ts +121 -1
  11. package/Server/Services/AnalyticsDatabaseService.ts +19 -23
  12. package/Server/Services/IncidentService.ts +179 -252
  13. package/Server/Services/IncidentStateTimelineService.ts +120 -1
  14. package/Server/Services/Index.ts +2 -0
  15. package/Server/Services/MetricAggregationService.ts +73 -3
  16. package/Server/Services/MetricService.ts +228 -0
  17. package/Server/Services/MutableMetricService.ts +275 -0
  18. package/Server/Services/TelemetryAttributeService.ts +186 -5
  19. package/Server/Utils/AnalyticsDatabase/ClusterConfig.ts +2 -0
  20. package/Server/Utils/AnalyticsDatabase/Statement.ts +2 -0
  21. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +81 -2
  22. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +143 -0
  23. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +208 -0
  24. package/Tests/Types/Metrics/MetricPipelineRuleFilterCondition.test.ts +208 -0
  25. package/Tests/Types/Monitor/MonitorStepEntityScope.test.ts +0 -56
  26. package/Types/AnalyticsDatabase/AnalyticsTableEngine.ts +1 -0
  27. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +1 -0
  28. package/Types/AnalyticsDatabase/TableColumn.ts +16 -0
  29. package/Types/Monitor/MonitorStepMetricMonitor.ts +0 -27
  30. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +24 -0
  31. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  32. package/build/dist/Models/AnalyticsModels/Log.js +1 -0
  33. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  34. package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
  35. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  36. package/build/dist/Models/AnalyticsModels/MutableMetric.js +275 -0
  37. package/build/dist/Models/AnalyticsModels/MutableMetric.js.map +1 -0
  38. package/build/dist/Models/AnalyticsModels/Profile.js +1 -0
  39. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  40. package/build/dist/Models/AnalyticsModels/Span.js +1 -0
  41. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  42. package/build/dist/Server/Infrastructure/Queue.js +14 -2
  43. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  44. package/build/dist/Server/Infrastructure/QueueWorker.js +45 -1
  45. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  46. package/build/dist/Server/Services/AlertService.js +133 -170
  47. package/build/dist/Server/Services/AlertService.js.map +1 -1
  48. package/build/dist/Server/Services/AlertStateTimelineService.js +97 -0
  49. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  50. package/build/dist/Server/Services/AnalyticsDatabaseService.js +16 -15
  51. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  52. package/build/dist/Server/Services/IncidentService.js +142 -207
  53. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  54. package/build/dist/Server/Services/IncidentStateTimelineService.js +97 -0
  55. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  56. package/build/dist/Server/Services/Index.js +2 -0
  57. package/build/dist/Server/Services/Index.js.map +1 -1
  58. package/build/dist/Server/Services/MetricAggregationService.js +49 -3
  59. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  60. package/build/dist/Server/Services/MetricService.js +155 -0
  61. package/build/dist/Server/Services/MetricService.js.map +1 -1
  62. package/build/dist/Server/Services/MutableMetricService.js +182 -0
  63. package/build/dist/Server/Services/MutableMetricService.js.map +1 -0
  64. package/build/dist/Server/Services/TelemetryAttributeService.js +132 -7
  65. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  66. package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js +2 -0
  67. package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js.map +1 -1
  68. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +2 -0
  69. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  70. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +66 -2
  71. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  72. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js +1 -0
  73. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js.map +1 -1
  74. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +1 -0
  75. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  76. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  77. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  78. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js +0 -13
  79. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js.map +1 -1
  80. package/package.json +1 -1
@@ -591,6 +591,7 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
591
591
  required: true,
592
592
  defaultValue: {},
593
593
  type: TableColumnType.MapStringString,
594
+ mapKeysColumn: "attributeKeys",
594
595
  accessControl: {
595
596
  read: [
596
597
  Permission.ProjectOwner,
@@ -614,6 +615,23 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
614
615
  },
615
616
  });
616
617
 
618
+ const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
619
+ key: "attributeKeys",
620
+ codec: { codec: "ZSTD", level: 3 },
621
+ title: "Attribute Keys",
622
+ description: "Attribute keys extracted from attributes",
623
+ required: true,
624
+ defaultValue: [],
625
+ type: TableColumnType.ArrayText,
626
+ skipIndex: {
627
+ name: "idx_attribute_keys",
628
+ type: SkipIndexType.BloomFilter,
629
+ params: [0.01],
630
+ granularity: 1,
631
+ },
632
+ accessControl: attributesColumn.accessControl,
633
+ });
634
+
617
635
  const entityKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
618
636
  key: "entityKeys",
619
637
  codec: { codec: "ZSTD", level: 3 },
@@ -788,6 +806,7 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
788
806
  environmentColumn,
789
807
  parsedFramesColumn,
790
808
  attributesColumn,
809
+ attributeKeysColumn,
791
810
  entityKeysColumn,
792
811
  ...scalarEntityKeyColumns,
793
812
  retentionDateColumn,
@@ -918,6 +937,14 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
918
937
  this.setColumnValue("attributes", v);
919
938
  }
920
939
 
940
+ public get attributeKeys(): Array<string> | undefined {
941
+ return this.getColumnValue("attributeKeys") as Array<string> | undefined;
942
+ }
943
+
944
+ public set attributeKeys(v: Array<string> | undefined) {
945
+ this.setColumnValue("attributeKeys", v);
946
+ }
947
+
921
948
  public get entityKeys(): Array<string> | undefined {
922
949
  return this.getColumnValue("entityKeys") as Array<string> | undefined;
923
950
  }
@@ -253,6 +253,7 @@ export default class Log extends AnalyticsBaseModel {
253
253
  required: true,
254
254
  defaultValue: {},
255
255
  type: TableColumnType.MapStringString,
256
+ mapKeysColumn: "attributeKeys",
256
257
  accessControl: {
257
258
  read: [
258
259
  Permission.ProjectOwner,
@@ -367,6 +367,7 @@ export default class Metric extends AnalyticsBaseModel {
367
367
  required: true,
368
368
  type: TableColumnType.MapStringString,
369
369
  defaultValue: {},
370
+ mapKeysColumn: "attributeKeys",
370
371
  accessControl: {
371
372
  read: [
372
373
  Permission.ProjectOwner,
@@ -0,0 +1,333 @@
1
+ import AnalyticsBaseModel from "./AnalyticsBaseModel/AnalyticsBaseModel";
2
+ import { MetricPointType } from "./Metric";
3
+ import AnalyticsTableEngine from "../../Types/AnalyticsDatabase/AnalyticsTableEngine";
4
+ import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
5
+ import AnalyticsTableColumn, {
6
+ SkipIndexType,
7
+ } from "../../Types/AnalyticsDatabase/TableColumn";
8
+ import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
9
+ import { JSONObject } from "../../Types/JSON";
10
+ import ObjectID from "../../Types/ObjectID";
11
+ import ServiceType from "../../Types/Telemetry/ServiceType";
12
+
13
+ export default class MutableMetric extends AnalyticsBaseModel {
14
+ public constructor() {
15
+ const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
16
+ key: "projectId",
17
+ title: "Project ID",
18
+ description: "ID of project",
19
+ required: true,
20
+ type: TableColumnType.ObjectID,
21
+ isTenantId: true,
22
+ });
23
+
24
+ const primaryEntityIdColumn: AnalyticsTableColumn =
25
+ new AnalyticsTableColumn({
26
+ key: "primaryEntityId",
27
+ title: "Primary Entity ID",
28
+ description: "ID of the incident, alert, or other entity.",
29
+ required: true,
30
+ type: TableColumnType.ObjectID,
31
+ });
32
+
33
+ const primaryEntityTypeColumn: AnalyticsTableColumn =
34
+ new AnalyticsTableColumn({
35
+ key: "primaryEntityType",
36
+ isLowCardinality: true,
37
+ title: "Primary Entity Type",
38
+ description: "Type of the primary entity.",
39
+ required: true,
40
+ type: TableColumnType.Text,
41
+ skipIndex: {
42
+ name: "idx_mutable_metric_entity_type",
43
+ type: SkipIndexType.Set,
44
+ params: [10],
45
+ granularity: 4,
46
+ },
47
+ });
48
+
49
+ const nameColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
50
+ key: "name",
51
+ codec: { codec: "ZSTD", level: 1 },
52
+ title: "Name",
53
+ description: "Name of the metric.",
54
+ required: true,
55
+ type: TableColumnType.Text,
56
+ skipIndex: {
57
+ name: "idx_mutable_metric_name",
58
+ type: SkipIndexType.BloomFilter,
59
+ params: [0.01],
60
+ granularity: 1,
61
+ },
62
+ });
63
+
64
+ const metricPointIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
65
+ key: "metricPointId",
66
+ codec: { codec: "ZSTD", level: 1 },
67
+ title: "Metric Point ID",
68
+ description:
69
+ "Stable identity for the mutable metric point within its entity and metric name.",
70
+ required: true,
71
+ type: TableColumnType.Text,
72
+ });
73
+
74
+ const metricPointTypeColumn: AnalyticsTableColumn =
75
+ new AnalyticsTableColumn({
76
+ key: "metricPointType",
77
+ isLowCardinality: true,
78
+ title: "Metric Point Type",
79
+ description: "Metric point type.",
80
+ required: false,
81
+ type: TableColumnType.Text,
82
+ skipIndex: {
83
+ name: "idx_mutable_metric_point_type",
84
+ type: SkipIndexType.Set,
85
+ params: [5],
86
+ granularity: 4,
87
+ },
88
+ });
89
+
90
+ const timeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
91
+ key: "time",
92
+ codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
93
+ title: "Time",
94
+ description: "When this metric point happened.",
95
+ required: true,
96
+ type: TableColumnType.DateTime64,
97
+ });
98
+
99
+ const timeUnixNanoColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
100
+ key: "timeUnixNano",
101
+ codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
102
+ title: "Time (in Unix Nano)",
103
+ description: "When this metric point happened.",
104
+ required: true,
105
+ type: TableColumnType.UInt64,
106
+ });
107
+
108
+ const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
109
+ key: "attributes",
110
+ codec: { codec: "ZSTD", level: 3 },
111
+ title: "Attributes",
112
+ description: "Metric attributes.",
113
+ required: true,
114
+ type: TableColumnType.MapStringString,
115
+ defaultValue: {},
116
+ mapKeysColumn: "attributeKeys",
117
+ });
118
+
119
+ const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
120
+ key: "attributeKeys",
121
+ codec: { codec: "ZSTD", level: 3 },
122
+ title: "Attribute Keys",
123
+ description: "Attribute keys extracted from attributes.",
124
+ required: true,
125
+ defaultValue: [],
126
+ type: TableColumnType.ArrayText,
127
+ skipIndex: {
128
+ name: "idx_mutable_metric_attribute_keys",
129
+ type: SkipIndexType.BloomFilter,
130
+ params: [0.01],
131
+ granularity: 1,
132
+ },
133
+ });
134
+
135
+ const valueColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
136
+ key: "value",
137
+ codec: [{ codec: "Gorilla" }, { codec: "ZSTD", level: 1 }],
138
+ title: "Value",
139
+ description: "Metric value.",
140
+ required: false,
141
+ type: TableColumnType.Decimal,
142
+ });
143
+
144
+ const versionColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
145
+ key: "version",
146
+ codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
147
+ title: "Version",
148
+ description:
149
+ "Monotonic version for ReplacingMergeTree and version-aware reads.",
150
+ required: true,
151
+ type: TableColumnType.UInt64,
152
+ });
153
+
154
+ const isDeletedColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
155
+ key: "isDeleted",
156
+ title: "Is Deleted",
157
+ description: "Tombstone marker for this metric point identity.",
158
+ required: true,
159
+ type: TableColumnType.Boolean,
160
+ defaultValue: false,
161
+ });
162
+
163
+ const retentionDateColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
164
+ key: "retentionDate",
165
+ codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
166
+ title: "Retention Date",
167
+ description: "Date after which this row is eligible for TTL deletion.",
168
+ required: true,
169
+ type: TableColumnType.Date,
170
+ });
171
+
172
+ super({
173
+ tableName: AnalyticsTableName.MutableMetric,
174
+ tableEngine: AnalyticsTableEngine.ReplacingMergeTree,
175
+ singularName: "Mutable Metric",
176
+ pluralName: "Mutable Metrics",
177
+ tableDescription:
178
+ "Versioned derived metric points for lifecycle metrics that can be corrected after initial emission.",
179
+ tableColumns: [
180
+ projectIdColumn,
181
+ primaryEntityIdColumn,
182
+ primaryEntityTypeColumn,
183
+ nameColumn,
184
+ metricPointIdColumn,
185
+ metricPointTypeColumn,
186
+ timeColumn,
187
+ timeUnixNanoColumn,
188
+ attributesColumn,
189
+ attributeKeysColumn,
190
+ valueColumn,
191
+ versionColumn,
192
+ isDeletedColumn,
193
+ retentionDateColumn,
194
+ ],
195
+ projections: [],
196
+ sortKeys: [
197
+ "projectId",
198
+ "name",
199
+ "primaryEntityType",
200
+ "primaryEntityId",
201
+ "metricPointId",
202
+ ],
203
+ primaryKeys: [
204
+ "projectId",
205
+ "name",
206
+ "primaryEntityType",
207
+ "primaryEntityId",
208
+ "metricPointId",
209
+ ],
210
+ partitionKey: "toYYYYMM(retentionDate)",
211
+ shardingKey:
212
+ "cityHash64(projectId, name, primaryEntityType, primaryEntityId, metricPointId)",
213
+ tableSettings:
214
+ "ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
215
+ ttlExpression: "retentionDate DELETE",
216
+ defaultSortColumn: "time",
217
+ });
218
+ }
219
+
220
+ public get projectId(): ObjectID | undefined {
221
+ return this.getColumnValue("projectId") as ObjectID | undefined;
222
+ }
223
+
224
+ public set projectId(v: ObjectID | undefined) {
225
+ this.setColumnValue("projectId", v);
226
+ }
227
+
228
+ public get primaryEntityId(): ObjectID | undefined {
229
+ return this.getColumnValue("primaryEntityId") as ObjectID | undefined;
230
+ }
231
+
232
+ public set primaryEntityId(v: ObjectID | undefined) {
233
+ this.setColumnValue("primaryEntityId", v);
234
+ }
235
+
236
+ public get primaryEntityType(): ServiceType | undefined {
237
+ return this.getColumnValue("primaryEntityType") as ServiceType | undefined;
238
+ }
239
+
240
+ public set primaryEntityType(v: ServiceType | undefined) {
241
+ this.setColumnValue("primaryEntityType", v);
242
+ }
243
+
244
+ public get name(): string | undefined {
245
+ return this.getColumnValue("name") as string | undefined;
246
+ }
247
+
248
+ public set name(v: string | undefined) {
249
+ this.setColumnValue("name", v);
250
+ }
251
+
252
+ public get metricPointId(): string | undefined {
253
+ return this.getColumnValue("metricPointId") as string | undefined;
254
+ }
255
+
256
+ public set metricPointId(v: string | undefined) {
257
+ this.setColumnValue("metricPointId", v);
258
+ }
259
+
260
+ public get metricPointType(): MetricPointType | undefined {
261
+ return this.getColumnValue("metricPointType") as
262
+ | MetricPointType
263
+ | undefined;
264
+ }
265
+
266
+ public set metricPointType(v: MetricPointType | undefined) {
267
+ this.setColumnValue("metricPointType", v);
268
+ }
269
+
270
+ public get time(): Date | undefined {
271
+ return this.getColumnValue("time") as Date | undefined;
272
+ }
273
+
274
+ public set time(v: Date | undefined) {
275
+ this.setColumnValue("time", v);
276
+ }
277
+
278
+ public get timeUnixNano(): number | undefined {
279
+ return this.getColumnValue("timeUnixNano") as number | undefined;
280
+ }
281
+
282
+ public set timeUnixNano(v: number | undefined) {
283
+ this.setColumnValue("timeUnixNano", v);
284
+ }
285
+
286
+ public get attributes(): JSONObject | undefined {
287
+ return this.getColumnValue("attributes") as JSONObject | undefined;
288
+ }
289
+
290
+ public set attributes(v: JSONObject | undefined) {
291
+ this.setColumnValue("attributes", v);
292
+ }
293
+
294
+ public get attributeKeys(): Array<string> | undefined {
295
+ return this.getColumnValue("attributeKeys") as Array<string> | undefined;
296
+ }
297
+
298
+ public set attributeKeys(v: Array<string> | undefined) {
299
+ this.setColumnValue("attributeKeys", v);
300
+ }
301
+
302
+ public get value(): number | undefined {
303
+ return this.getColumnValue("value") as number | undefined;
304
+ }
305
+
306
+ public set value(v: number | undefined) {
307
+ this.setColumnValue("value", v);
308
+ }
309
+
310
+ public get version(): number | undefined {
311
+ return this.getColumnValue("version") as number | undefined;
312
+ }
313
+
314
+ public set version(v: number | undefined) {
315
+ this.setColumnValue("version", v);
316
+ }
317
+
318
+ public get isDeleted(): boolean | undefined {
319
+ return this.getColumnValue("isDeleted") as boolean | undefined;
320
+ }
321
+
322
+ public set isDeleted(v: boolean | undefined) {
323
+ this.setColumnValue("isDeleted", v);
324
+ }
325
+
326
+ public get retentionDate(): Date | undefined {
327
+ return this.getColumnValue("retentionDate") as Date | undefined;
328
+ }
329
+
330
+ public set retentionDate(v: Date | undefined) {
331
+ this.setColumnValue("retentionDate", v);
332
+ }
333
+ }
@@ -514,6 +514,7 @@ export default class Profile extends AnalyticsBaseModel {
514
514
  required: true,
515
515
  defaultValue: {},
516
516
  type: TableColumnType.MapStringString,
517
+ mapKeysColumn: "attributeKeys",
517
518
  accessControl: {
518
519
  read: [
519
520
  Permission.ProjectOwner,
@@ -464,6 +464,7 @@ export default class Span extends AnalyticsBaseModel {
464
464
  required: true,
465
465
  defaultValue: {},
466
466
  type: TableColumnType.MapStringString,
467
+ mapKeysColumn: "attributeKeys",
467
468
  accessControl: {
468
469
  read: [
469
470
  Permission.ProjectOwner,
@@ -455,6 +455,23 @@ export default class Queue {
455
455
  }
456
456
  }
457
457
 
458
+ private static normalizeFailedReason(reason: string | undefined): string {
459
+ if (typeof reason !== "string") {
460
+ return "No reason provided";
461
+ }
462
+
463
+ const normalized: string = reason.trim().toLowerCase();
464
+ if (
465
+ normalized.length === 0 ||
466
+ normalized === "null" ||
467
+ normalized === "undefined"
468
+ ) {
469
+ return "No reason provided";
470
+ }
471
+
472
+ return reason;
473
+ }
474
+
458
475
  @CaptureSpan()
459
476
  public static async getQueueSize(queueName: QueueName): Promise<number> {
460
477
  const queue: BullQueue = this.getQueue(queueName);
@@ -534,7 +551,7 @@ export default class Queue {
534
551
  id: job.id || "unknown",
535
552
  name: job.name || "unknown",
536
553
  data: job.data as JSONObject,
537
- failedReason: job.failedReason || "No reason provided",
554
+ failedReason: Queue.normalizeFailedReason(job.failedReason),
538
555
  processedOn: job.processedOn ? new Date(job.processedOn) : null,
539
556
  finishedOn: job.finishedOn ? new Date(job.finishedOn) : null,
540
557
  attemptsMade: job.attemptsMade || 0,
@@ -641,7 +658,7 @@ export default class Queue {
641
658
  opts: (job.opts as unknown as JSONObject) || {},
642
659
  returnValue: job.returnvalue ?? null,
643
660
  progress: (job.progress as number | Record<string, unknown>) ?? null,
644
- failedReason: job.failedReason || "No reason provided",
661
+ failedReason: Queue.normalizeFailedReason(job.failedReason),
645
662
  stackTrace: job.stacktrace || [],
646
663
  logs: logs,
647
664
  attemptsMade: job.attemptsMade || 0,
@@ -92,7 +92,10 @@ export default class QueueWorker {
92
92
  (err as { name?: string })?.name === "TimeoutException"
93
93
  ? "timeout"
94
94
  : "failure";
95
- throw err;
95
+ throw QueueWorker.toReportableError(
96
+ err,
97
+ `${queueName}/${job.name || "unknown"} (job ${job.id ?? "unknown"})`,
98
+ );
96
99
  } finally {
97
100
  const elapsedNs: bigint = process.hrtime.bigint() - startNs;
98
101
  const durationMs: number = Number(elapsedNs) / 1e6;
@@ -134,6 +137,69 @@ export default class QueueWorker {
134
137
  return worker;
135
138
  }
136
139
 
140
+ private static toReportableError(err: unknown, context: string): unknown {
141
+ const messageIsUseful: (value: unknown) => boolean = (
142
+ value: unknown,
143
+ ): boolean => {
144
+ const message: unknown = (value as { message?: unknown })?.message;
145
+ if (typeof message !== "string") {
146
+ return false;
147
+ }
148
+
149
+ const normalized: string = message.trim().toLowerCase();
150
+ return (
151
+ normalized.length > 0 &&
152
+ normalized !== "null" &&
153
+ normalized !== "undefined"
154
+ );
155
+ };
156
+
157
+ if (err instanceof Error && messageIsUseful(err)) {
158
+ return err;
159
+ }
160
+
161
+ const detailParts: Array<string> = [];
162
+
163
+ const name: unknown = (err as { name?: unknown })?.name;
164
+ if (typeof name === "string" && name && name !== "Error") {
165
+ detailParts.push(name);
166
+ }
167
+
168
+ const rawMessage: unknown = (err as { message?: unknown })?.message;
169
+ if (typeof rawMessage === "string" && rawMessage.trim()) {
170
+ detailParts.push(rawMessage.trim());
171
+ }
172
+
173
+ if (detailParts.length === 0) {
174
+ let serialized: string;
175
+ try {
176
+ serialized =
177
+ typeof err === "object" && err !== null
178
+ ? JSON.stringify(err)
179
+ : String(err);
180
+ } catch {
181
+ serialized = Object.prototype.toString.call(err);
182
+ }
183
+
184
+ detailParts.push(
185
+ serialized && serialized !== "{}" ? serialized : String(err),
186
+ );
187
+ }
188
+
189
+ const reportable: Error = new Error(
190
+ `${context} failed with a non-descriptive error (original message was empty/null). Detail: ${detailParts.join(
191
+ ": ",
192
+ )}`,
193
+ );
194
+
195
+ const originalStack: unknown = (err as { stack?: unknown })?.stack;
196
+ if (typeof originalStack === "string" && originalStack) {
197
+ reportable.stack = originalStack;
198
+ }
199
+
200
+ return reportable;
201
+ }
202
+
137
203
  @CaptureSpan()
138
204
  public static async runJobWithTimeout(
139
205
  timeoutInMS: number,