feat: alerts open bugs, and repeats of the same problem go to that bug
Owners and admins turn on a secret URL per project (Settings →
Integrations), POST /api/webhooks/alerts/{project}/{token}, that Sentry,
Uptime Kuma, Prometheus Alertmanager, Grafana or any tool sending JSON
with a title can call. A new problem opens a bug as TaskStream (label
«alerta», chosen type and priority); while that bug is open, the same
fingerprint goes to it (a comment at most every 30 minutes) and
«resolved» is said on it; once it is closed, a new bug names the old
one. Alerts of a project are handled one at a time, so two at once open
one bug, and a bug not yet projected still counts as open. Unknown
project, alerts off or a bad token all answer 403.
TaskStream may now comment as the system actor, like it already opened
tickets for public proposals. Stored in Mongo (alert_integrations,
alert_traces), not in the event store.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import {
|
||||
AlertIntegrationConfig,
|
||||
AlertIntegrationsRepository,
|
||||
AlertTrace,
|
||||
} from '@task-stream/api/tracker/application';
|
||||
import { Model } from 'mongoose';
|
||||
import { ReadModels } from './read-model.schemas';
|
||||
|
||||
type Stored<T> = T & { _id?: unknown };
|
||||
|
||||
@Injectable()
|
||||
export class MongoAlertIntegrationsRepository extends AlertIntegrationsRepository {
|
||||
constructor(
|
||||
@InjectModel(ReadModels.AlertIntegration)
|
||||
private readonly configs: Model<Stored<AlertIntegrationConfig>>,
|
||||
@InjectModel(ReadModels.AlertTrace)
|
||||
private readonly traces: Model<Stored<AlertTrace>>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async find(projectId: string): Promise<AlertIntegrationConfig | null> {
|
||||
const doc = await this.configs
|
||||
.findOne({ projectId })
|
||||
.lean<Stored<AlertIntegrationConfig>>();
|
||||
if (!doc) return null;
|
||||
const { _id: _ignored, ...config } = doc;
|
||||
return config;
|
||||
}
|
||||
|
||||
async save(config: AlertIntegrationConfig): Promise<void> {
|
||||
await this.configs.updateOne(
|
||||
{ projectId: config.projectId },
|
||||
{ $set: config },
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
async remove(projectId: string): Promise<void> {
|
||||
// The traces stay: turned on again, repeats still find their bug.
|
||||
await this.configs.deleteOne({ projectId });
|
||||
}
|
||||
|
||||
async trace(
|
||||
projectId: string,
|
||||
fingerprint: string,
|
||||
): Promise<AlertTrace | null> {
|
||||
const doc = await this.traces
|
||||
.findOne({ projectId, fingerprint })
|
||||
.lean<Stored<AlertTrace>>();
|
||||
if (!doc) return null;
|
||||
const { _id: _ignored, ...trace } = doc;
|
||||
return trace;
|
||||
}
|
||||
|
||||
async saveTrace(trace: AlertTrace): Promise<void> {
|
||||
await this.traces.updateOne(
|
||||
{ projectId: trace.projectId, fingerprint: trace.fingerprint },
|
||||
{ $set: trace },
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,8 @@ export const ReadModels = {
|
||||
SavedFilter: 'SavedFilter',
|
||||
MyView: 'MyView',
|
||||
PinnedIssue: 'PinnedIssue',
|
||||
AlertIntegration: 'AlertIntegration',
|
||||
AlertTrace: 'AlertTrace',
|
||||
Sprint: 'SprintReadModel',
|
||||
GitIntegration: 'GitIntegration',
|
||||
OutgoingWebhook: 'OutgoingWebhook',
|
||||
@@ -339,6 +341,25 @@ export const PinnedIssueSchema =
|
||||
SchemaFactory.createForClass(PinnedIssueDocument);
|
||||
PinnedIssueSchema.index({ ownerId: 1, issueId: 1 }, { unique: true });
|
||||
|
||||
@Schema({ ...LOOSE, collection: 'alert_integrations' })
|
||||
class AlertIntegrationDocument {
|
||||
@Prop({ type: String, required: true, unique: true }) projectId!: string;
|
||||
@Prop({ type: String }) token!: string;
|
||||
}
|
||||
export const AlertIntegrationSchema = SchemaFactory.createForClass(
|
||||
AlertIntegrationDocument,
|
||||
);
|
||||
|
||||
@Schema({ ...LOOSE, collection: 'alert_traces' })
|
||||
class AlertTraceDocument {
|
||||
@Prop({ type: String }) projectId!: string;
|
||||
@Prop({ type: String }) fingerprint!: string;
|
||||
@Prop({ type: String }) issueId!: string;
|
||||
}
|
||||
export const AlertTraceSchema =
|
||||
SchemaFactory.createForClass(AlertTraceDocument);
|
||||
AlertTraceSchema.index({ projectId: 1, fingerprint: 1 }, { unique: true });
|
||||
|
||||
@Schema({ ...LOOSE, collection: 'git_integrations' })
|
||||
class GitIntegrationDocument {
|
||||
/** The project's id. */
|
||||
@@ -442,6 +463,8 @@ export const READ_MODEL_SCHEMAS = [
|
||||
{ name: ReadModels.SavedFilter, schema: SavedFilterSchema },
|
||||
{ name: ReadModels.MyView, schema: MyViewSchema },
|
||||
{ name: ReadModels.PinnedIssue, schema: PinnedIssueSchema },
|
||||
{ name: ReadModels.AlertIntegration, schema: AlertIntegrationSchema },
|
||||
{ name: ReadModels.AlertTrace, schema: AlertTraceSchema },
|
||||
{ name: ReadModels.CalendarFeed, schema: CalendarFeedSchema },
|
||||
{ name: ReadModels.DashboardShare, schema: DashboardShareSchema },
|
||||
{ name: ReadModels.PushSubscription, schema: PushSubscriptionSchema },
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
SavedFiltersRepository,
|
||||
MyViewsRepository,
|
||||
PinsRepository,
|
||||
AlertIntegrationsRepository,
|
||||
GitIntegrationsRepository,
|
||||
OutgoingWebhooksRepository,
|
||||
AutomationsRepository,
|
||||
@@ -80,6 +81,7 @@ import { WebPushSender } from './push/web-push.sender';
|
||||
import { MongoSavedFiltersRepository } from './mongo/saved-filters.repository';
|
||||
import { MongoMyViewsRepository } from './mongo/my-views.repository';
|
||||
import { MongoPinsRepository } from './mongo/pins.repository';
|
||||
import { MongoAlertIntegrationsRepository } from './mongo/alert-integrations.repository';
|
||||
import { MongoGitIntegrationsRepository } from './mongo/git-integrations.repository';
|
||||
import { MongoOutgoingWebhooksRepository } from './mongo/outgoing-webhooks.repository';
|
||||
import { HttpWebhookSender } from './webhooks/http-webhook.sender';
|
||||
@@ -157,6 +159,10 @@ const PORTS = [
|
||||
{ provide: SavedFiltersRepository, useClass: MongoSavedFiltersRepository },
|
||||
{ provide: MyViewsRepository, useClass: MongoMyViewsRepository },
|
||||
{ provide: PinsRepository, useClass: MongoPinsRepository },
|
||||
{
|
||||
provide: AlertIntegrationsRepository,
|
||||
useClass: MongoAlertIntegrationsRepository,
|
||||
},
|
||||
{
|
||||
provide: GitIntegrationsRepository,
|
||||
useClass: MongoGitIntegrationsRepository,
|
||||
|
||||
Reference in New Issue
Block a user