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,71 @@
|
||||
import { Args, ID, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { User } from '@task-stream/api/identity/domain';
|
||||
import { CurrentUser } from '@task-stream/api/identity/presenters';
|
||||
import {
|
||||
AlertIntegration,
|
||||
AlertsService,
|
||||
} from '@task-stream/api/tracker/application';
|
||||
import { alertWebhookPath } from '../common/alert-webhook';
|
||||
import { AlertSettingsInput } from './inputs';
|
||||
import { AlertIntegrationGql } from './types';
|
||||
|
||||
/** Alerts → bugs: the project's secret URL (owners and admins configure it). */
|
||||
@Resolver(() => AlertIntegrationGql)
|
||||
export class AlertResolver {
|
||||
constructor(private readonly alerts: AlertsService) {}
|
||||
|
||||
@Query(() => AlertIntegrationGql)
|
||||
async alertIntegration(
|
||||
@CurrentUser() me: User,
|
||||
@Args('projectId', { type: () => ID }) projectId: string,
|
||||
) {
|
||||
return withPath(projectId, await this.alerts.get(me.id, projectId));
|
||||
}
|
||||
|
||||
@Mutation(() => AlertIntegrationGql, {
|
||||
description:
|
||||
'Turn alerts → bugs on, or change the type and priority it opens (the URL stays)',
|
||||
})
|
||||
async saveAlertIntegration(
|
||||
@CurrentUser() me: User,
|
||||
@Args('projectId', { type: () => ID }) projectId: string,
|
||||
@Args('input') input: AlertSettingsInput,
|
||||
) {
|
||||
return withPath(
|
||||
projectId,
|
||||
await this.alerts.save(me.id, projectId, {
|
||||
typeId: input.typeId,
|
||||
priority: input.priority,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => AlertIntegrationGql, {
|
||||
description: 'A new secret URL; the old one stops working at once',
|
||||
})
|
||||
async rotateAlertToken(
|
||||
@CurrentUser() me: User,
|
||||
@Args('projectId', { type: () => ID }) projectId: string,
|
||||
) {
|
||||
return withPath(projectId, await this.alerts.rotateToken(me.id, projectId));
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Turn it off; the bugs it opened stay',
|
||||
})
|
||||
async disableAlertIntegration(
|
||||
@CurrentUser() me: User,
|
||||
@Args('projectId', { type: () => ID }) projectId: string,
|
||||
) {
|
||||
await this.alerts.disable(me.id, projectId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function withPath(projectId: string, integration: AlertIntegration) {
|
||||
const { token, ...rest } = integration;
|
||||
return {
|
||||
...rest,
|
||||
webhookPath: token ? alertWebhookPath(projectId, token) : null,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user