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>
72 lines
2.2 KiB
TypeScript
72 lines
2.2 KiB
TypeScript
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,
|
|
};
|
|
}
|