@oneuptime/common 11.3.5 → 11.3.6
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.
- package/Server/Infrastructure/Queue.ts +103 -16
- package/Server/Services/AnalyticsDatabaseService.ts +9 -5
- package/Server/Services/TeamMemberService.ts +38 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +31 -0
- package/Server/Utils/Monitor/IncomingRequestIncidentGrouping.ts +428 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +158 -8
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +26 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +147 -2
- package/Server/Utils/Monitor/MonitorResource.ts +74 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +2 -1
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +32 -0
- package/Tests/Server/Utils/Monitor/IncomingRequestIncidentGrouping.test.ts +389 -0
- package/Tests/Server/Utils/Monitor/SeriesAbsenceResolutionGuard.test.ts +177 -0
- package/Tests/Types/Events/Recurring.test.ts +475 -0
- package/Tests/Types/Monitor/CriteriaFilter.test.ts +348 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +331 -0
- package/Tests/UI/Components/Alert.test.tsx +23 -15
- package/Tests/UI/Components/Breadcrumbs.test.tsx +18 -6
- package/Tests/UI/Components/Button.test.tsx +3 -3
- package/Tests/UI/Components/DictionaryFilterOperator.test.ts +174 -0
- package/Tests/UI/Components/EmptyState/EmptyState.test.tsx +4 -3
- package/Tests/UI/Components/Input.test.tsx +3 -2
- package/Tests/UI/Components/Pill.test.tsx +4 -2
- package/Tests/UI/Components/SideMenuItem.test.tsx +18 -8
- package/Tests/UI/Components/TextArea.test.tsx +6 -4
- package/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.ts +74 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +22 -0
- package/UI/Components/Dictionary/Dictionary.tsx +123 -50
- package/UI/Components/Dictionary/DictionaryFilterOperator.ts +96 -1
- package/UI/Components/MasterPage/MasterPage.tsx +5 -3
- package/build/dist/Server/Infrastructure/Queue.js +83 -15
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +9 -5
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +37 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +26 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js +288 -0
- package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +119 -8
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +24 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +103 -2
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +62 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js +8 -0
- package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +10 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +60 -9
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +69 -0
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +5 -3
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -88,8 +88,8 @@ export default class Queue {
|
|
|
88
88
|
// Keep BullMQ data under control to avoid Redis bloat
|
|
89
89
|
defaultJobOptions: {
|
|
90
90
|
// keep only recent completed/failed jobs
|
|
91
|
-
removeOnComplete: { count: 500 }, // keep last
|
|
92
|
-
removeOnFail: { count: 100 }, // keep last
|
|
91
|
+
removeOnComplete: { count: 500 }, // keep last 500 completed jobs
|
|
92
|
+
removeOnFail: { count: 100 }, // keep last 100 failed jobs
|
|
93
93
|
},
|
|
94
94
|
/*
|
|
95
95
|
* Optionally cap the event stream length (supported in BullMQ >= v5)
|
|
@@ -112,24 +112,111 @@ export default class Queue {
|
|
|
112
112
|
logger.error(err);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
await queue.clean(oneDaysMs, 1000, "failed");
|
|
124
|
-
} catch {
|
|
125
|
-
// ignore cleanup errors to not impact normal flow
|
|
126
|
-
}
|
|
127
|
-
})();
|
|
128
|
-
}
|
|
115
|
+
/*
|
|
116
|
+
* Lazy fallback for the startup sweep: the first time a queue is created in
|
|
117
|
+
* this process, kick off its once-per-process terminal-job cleanup. The
|
|
118
|
+
* eager path (cleanAllQueuesOnStartup, called at service boot) normally
|
|
119
|
+
* wins; whichever runs first is the only one to sweep, via the gate inside
|
|
120
|
+
* cleanQueueOnStartup(). Fire and forget so it never blocks enqueue.
|
|
121
|
+
*/
|
|
122
|
+
void this.cleanQueueOnStartup(queueName);
|
|
129
123
|
|
|
130
124
|
return queue;
|
|
131
125
|
}
|
|
132
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Eagerly sweeps every queue's terminal (completed/failed) jobs at process
|
|
129
|
+
* startup, so the Completed/Failed counts on the admin Health page reset on a
|
|
130
|
+
* pod (re)start regardless of when — or whether — each queue next produces a
|
|
131
|
+
* job. Call once during service boot (see App/Index.ts). Each queue is swept
|
|
132
|
+
* at most once per process via the cleanedQueueNames gate, so this is safe to
|
|
133
|
+
* call alongside the lazy getQueue() path and from any service role.
|
|
134
|
+
*/
|
|
135
|
+
@CaptureSpan()
|
|
136
|
+
public static async cleanAllQueuesOnStartup(): Promise<void> {
|
|
137
|
+
await Promise.all(
|
|
138
|
+
Object.values(QueueName).map((queueName: QueueName) => {
|
|
139
|
+
return this.cleanQueueOnStartup(queueName);
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* How old (in ms) a completed/failed job must be before the startup sweep
|
|
146
|
+
* removes it. 0 means "remove all terminal jobs on every restart", which
|
|
147
|
+
* resets the Completed/Failed counts on the admin Health page so operators
|
|
148
|
+
* stop seeing stale failures after a deploy/restart. Raise this (e.g. to
|
|
149
|
+
* 24 * 60 * 60 * 1000) if you'd rather keep recent failures around for
|
|
150
|
+
* crash-loop debugging.
|
|
151
|
+
*/
|
|
152
|
+
private static readonly STARTUP_CLEANUP_GRACE_MS: number = 0;
|
|
153
|
+
// Number of jobs removed per clean() call; we loop until the state is drained.
|
|
154
|
+
private static readonly STARTUP_CLEANUP_BATCH_SIZE: number = 1000;
|
|
155
|
+
// Safety cap so a pathologically large backlog can't loop forever.
|
|
156
|
+
private static readonly STARTUP_CLEANUP_MAX_BATCHES: number = 1000;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Removes terminal (completed/failed) jobs for a single queue, at most once
|
|
160
|
+
* per process (gated by cleanedQueueNames). Drains in batches because
|
|
161
|
+
* BullMQ's clean() caps each call at `limit` removals. Leaves live states
|
|
162
|
+
* (waiting/active/delayed) untouched. Best-effort: any error is logged and
|
|
163
|
+
* swallowed so a transient Redis hiccup at boot never breaks queue setup.
|
|
164
|
+
*/
|
|
165
|
+
private static async cleanQueueOnStartup(
|
|
166
|
+
queueName: QueueName,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
/*
|
|
169
|
+
* Gate synchronously, before any await, so the lazy getQueue() trigger and
|
|
170
|
+
* the eager cleanAllQueuesOnStartup() call can't both sweep the same queue.
|
|
171
|
+
*/
|
|
172
|
+
if (this.cleanedQueueNames.has(queueName)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.cleanedQueueNames.add(queueName);
|
|
176
|
+
|
|
177
|
+
const queue: BullQueue = this.getQueue(queueName);
|
|
178
|
+
|
|
179
|
+
const terminalStates: Array<"completed" | "failed"> = [
|
|
180
|
+
"completed",
|
|
181
|
+
"failed",
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (const state of terminalStates) {
|
|
185
|
+
try {
|
|
186
|
+
let removedTotal: number = 0;
|
|
187
|
+
|
|
188
|
+
for (
|
|
189
|
+
let batch: number = 0;
|
|
190
|
+
batch < this.STARTUP_CLEANUP_MAX_BATCHES;
|
|
191
|
+
batch++
|
|
192
|
+
) {
|
|
193
|
+
const removed: string[] = await queue.clean(
|
|
194
|
+
this.STARTUP_CLEANUP_GRACE_MS,
|
|
195
|
+
this.STARTUP_CLEANUP_BATCH_SIZE,
|
|
196
|
+
state,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
removedTotal += removed.length;
|
|
200
|
+
|
|
201
|
+
// A short batch means the state is drained; stop looping.
|
|
202
|
+
if (removed.length < this.STARTUP_CLEANUP_BATCH_SIZE) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (removedTotal > 0) {
|
|
208
|
+
logger.debug(
|
|
209
|
+
`Queue ${queueName}: removed ${removedTotal} ${state} job(s) on startup`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
// Ignore cleanup errors to not impact normal flow.
|
|
214
|
+
logger.debug(`Queue ${queueName}: startup ${state} cleanup failed`);
|
|
215
|
+
logger.debug(err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
133
220
|
@CaptureSpan()
|
|
134
221
|
public static async removeJob(
|
|
135
222
|
queueName: QueueName,
|
|
@@ -126,8 +126,10 @@ export const MigrationExecuteOptions: ClickhouseExecuteOptions = {
|
|
|
126
126
|
useMigrationConnection: true,
|
|
127
127
|
clickhouseSettings: {
|
|
128
128
|
send_progress_in_http_headers: 1,
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
/*
|
|
130
|
+
* Emit a progress header every 10s — well under the App pool's 58s and the
|
|
131
|
+
* migration pool's 30-minute idle ceiling.
|
|
132
|
+
*/
|
|
131
133
|
http_headers_progress_interval_ms: "10000",
|
|
132
134
|
},
|
|
133
135
|
};
|
|
@@ -487,9 +489,11 @@ export default class AnalyticsDatabaseService<
|
|
|
487
489
|
): Promise<void> {
|
|
488
490
|
const statement: Statement =
|
|
489
491
|
this.statementGenerator.toAddColumnStatement(column);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
492
|
+
/*
|
|
493
|
+
* Schema-sync / migration-only path: route through the migration pool so a
|
|
494
|
+
* column add that backfills a DEFAULT/MATERIALIZED expression on a large
|
|
495
|
+
* table is not destroyed at the App pool's 58s socket-idle timeout.
|
|
496
|
+
*/
|
|
493
497
|
await this.execute(statement, MigrationExecuteOptions);
|
|
494
498
|
|
|
495
499
|
// Add skip index separately (ClickHouse requires ADD INDEX as a separate ALTER statement)
|
|
@@ -28,6 +28,7 @@ import SubscriptionPlan, {
|
|
|
28
28
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
29
29
|
import Email from "../../Types/Email";
|
|
30
30
|
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
31
|
+
import Name from "../../Types/Name";
|
|
31
32
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
32
33
|
import ObjectID from "../../Types/ObjectID";
|
|
33
34
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
@@ -125,6 +126,15 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
|
|
125
126
|
if (createBy.miscDataProps && createBy.miscDataProps["email"]) {
|
|
126
127
|
const email: Email = new Email(createBy.miscDataProps["email"] as string);
|
|
127
128
|
|
|
129
|
+
/*
|
|
130
|
+
* Optional name supplied on the invite form. Used only to set the name on
|
|
131
|
+
* a brand-new user, or to backfill an existing user who has no name yet —
|
|
132
|
+
* we never overwrite a name the user has already set.
|
|
133
|
+
*/
|
|
134
|
+
const nameValue: string | undefined = createBy.miscDataProps["name"]
|
|
135
|
+
? (createBy.miscDataProps["name"] as string).trim()
|
|
136
|
+
: undefined;
|
|
137
|
+
|
|
128
138
|
let user: User | null = await UserService.findByEmail(email, {
|
|
129
139
|
isRoot: true,
|
|
130
140
|
});
|
|
@@ -133,13 +143,40 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
|
|
133
143
|
|
|
134
144
|
if (!user) {
|
|
135
145
|
isNewUser = true;
|
|
146
|
+
|
|
136
147
|
user = await UserService.createByEmail({
|
|
137
148
|
email,
|
|
138
|
-
name:
|
|
149
|
+
name: nameValue ? new Name(nameValue) : undefined,
|
|
139
150
|
props: {
|
|
140
151
|
isRoot: true,
|
|
141
152
|
},
|
|
142
153
|
});
|
|
154
|
+
} else if (nameValue) {
|
|
155
|
+
/*
|
|
156
|
+
* User already exists. Backfill their name only if they don't have one
|
|
157
|
+
* yet; if they already have a name, leave it untouched.
|
|
158
|
+
*/
|
|
159
|
+
const existingUser: User | null = await UserService.findOneById({
|
|
160
|
+
id: user.id!,
|
|
161
|
+
select: {
|
|
162
|
+
name: true,
|
|
163
|
+
},
|
|
164
|
+
props: {
|
|
165
|
+
isRoot: true,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (existingUser && !existingUser.name?.toString()) {
|
|
170
|
+
await UserService.updateOneById({
|
|
171
|
+
id: user.id!,
|
|
172
|
+
data: {
|
|
173
|
+
name: new Name(nameValue),
|
|
174
|
+
},
|
|
175
|
+
props: {
|
|
176
|
+
isRoot: true,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
143
180
|
}
|
|
144
181
|
|
|
145
182
|
createBy.data.userId = user.id!;
|
|
@@ -29,6 +29,7 @@ import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
|
|
|
29
29
|
import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
|
|
30
30
|
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
31
31
|
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
32
|
+
import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
|
|
32
33
|
import ObjectID from "../../../Types/ObjectID";
|
|
33
34
|
import IsNull from "../../../Types/BaseDatabase/IsNull";
|
|
34
35
|
import LessThan from "../../../Types/BaseDatabase/LessThan";
|
|
@@ -887,6 +888,36 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
887
888
|
continue;
|
|
888
889
|
}
|
|
889
890
|
|
|
891
|
+
/*
|
|
892
|
+
* Multi-value exclusion (IncludesNone / "is none of"):
|
|
893
|
+
* `attributes['k'] NOT IN (...)`. Map subscript returns '' for a
|
|
894
|
+
* missing key, so (like NotEqual above) rows lacking the attribute
|
|
895
|
+
* pass the NOT IN test, which matches "the value is none of these".
|
|
896
|
+
* An empty IncludesNone is treated as "All" — skip the predicate
|
|
897
|
+
* rather than emit `NOT IN ()`. Values bind via a fresh `Includes`
|
|
898
|
+
* since Statement only types `Includes` as Array(String).
|
|
899
|
+
*/
|
|
900
|
+
if (mapEntry instanceof IncludesNone) {
|
|
901
|
+
const excludeValues: Array<string> = (
|
|
902
|
+
(mapEntry as IncludesNone).values || []
|
|
903
|
+
).map((v: string | ObjectID | number) => {
|
|
904
|
+
return String(v);
|
|
905
|
+
});
|
|
906
|
+
if (excludeValues.length === 0) {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
whereStatement.append(
|
|
910
|
+
SQL`AND ${key}[${{
|
|
911
|
+
value: mapKey,
|
|
912
|
+
type: TableColumnType.Text,
|
|
913
|
+
}}] NOT IN ${{
|
|
914
|
+
value: new Includes(excludeValues),
|
|
915
|
+
type: TableColumnType.Text,
|
|
916
|
+
}}`,
|
|
917
|
+
);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
|
|
890
921
|
// Bare string/number/boolean — direct Map subscript.
|
|
891
922
|
whereStatement.append(
|
|
892
923
|
SQL`AND ${key}[${{
|