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,52 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
AlertIntegration,
|
||||
AlertsGateway,
|
||||
IssuePriority,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import {
|
||||
AlertIntegrationDocument,
|
||||
DisableAlertIntegrationDocument,
|
||||
RotateAlertTokenDocument,
|
||||
SaveAlertIntegrationDocument,
|
||||
} from '../graphql/generated';
|
||||
import { GraphqlOperations } from './graphql-operations';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlAlertsGateway extends AlertsGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
integration(projectId: string): Observable<AlertIntegration> {
|
||||
return this.gql
|
||||
.query(AlertIntegrationDocument, { projectId })
|
||||
.pipe(map((d) => d.alertIntegration as AlertIntegration));
|
||||
}
|
||||
|
||||
save(
|
||||
projectId: string,
|
||||
settings: { typeId: string | null; priority: IssuePriority },
|
||||
): Observable<AlertIntegration> {
|
||||
return this.gql
|
||||
.mutate(SaveAlertIntegrationDocument, {
|
||||
projectId,
|
||||
input: {
|
||||
typeId: settings.typeId,
|
||||
priority: settings.priority as never,
|
||||
},
|
||||
})
|
||||
.pipe(map((d) => d.saveAlertIntegration as AlertIntegration));
|
||||
}
|
||||
|
||||
rotateToken(projectId: string): Observable<AlertIntegration> {
|
||||
return this.gql
|
||||
.mutate(RotateAlertTokenDocument, { projectId })
|
||||
.pipe(map((d) => d.rotateAlertToken as AlertIntegration));
|
||||
}
|
||||
|
||||
disable(projectId: string): Observable<void> {
|
||||
return this.gql
|
||||
.mutate(DisableAlertIntegrationDocument, { projectId })
|
||||
.pipe(map(() => undefined));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
fragment AlertIntegrationFields on AlertIntegration {
|
||||
enabled
|
||||
webhookPath
|
||||
typeId
|
||||
priority
|
||||
}
|
||||
|
||||
query AlertIntegration($projectId: ID!) {
|
||||
alertIntegration(projectId: $projectId) {
|
||||
...AlertIntegrationFields
|
||||
}
|
||||
}
|
||||
|
||||
mutation SaveAlertIntegration($projectId: ID!, $input: AlertSettingsInput!) {
|
||||
saveAlertIntegration(projectId: $projectId, input: $input) {
|
||||
...AlertIntegrationFields
|
||||
}
|
||||
}
|
||||
|
||||
mutation RotateAlertToken($projectId: ID!) {
|
||||
rotateAlertToken(projectId: $projectId) {
|
||||
...AlertIntegrationFields
|
||||
}
|
||||
}
|
||||
|
||||
mutation DisableAlertIntegration($projectId: ID!) {
|
||||
disableAlertIntegration(projectId: $projectId)
|
||||
}
|
||||
@@ -12,6 +12,12 @@ export type AddChecklistItemInput = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type AlertSettingsInput = {
|
||||
priority?: IssuePriority | null | undefined;
|
||||
/** Ticket type to open; null: the project's first bug type */
|
||||
typeId?: string | null | undefined;
|
||||
};
|
||||
|
||||
export type AreaOptionInput = {
|
||||
archived?: boolean;
|
||||
id: string;
|
||||
@@ -424,6 +430,61 @@ export type WorkflowStatusInput = {
|
||||
wipLimit?: number | null | undefined;
|
||||
};
|
||||
|
||||
export type AlertIntegrationFieldsFragment = {
|
||||
enabled: boolean;
|
||||
webhookPath: string | null;
|
||||
typeId: string | null;
|
||||
priority: IssuePriority;
|
||||
};
|
||||
|
||||
export type AlertIntegrationQueryVariables = Exact<{
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
export type AlertIntegrationQuery = {
|
||||
alertIntegration: {
|
||||
enabled: boolean;
|
||||
webhookPath: string | null;
|
||||
typeId: string | null;
|
||||
priority: IssuePriority;
|
||||
};
|
||||
};
|
||||
|
||||
export type SaveAlertIntegrationMutationVariables = Exact<{
|
||||
projectId: string;
|
||||
input: AlertSettingsInput;
|
||||
}>;
|
||||
|
||||
export type SaveAlertIntegrationMutation = {
|
||||
saveAlertIntegration: {
|
||||
enabled: boolean;
|
||||
webhookPath: string | null;
|
||||
typeId: string | null;
|
||||
priority: IssuePriority;
|
||||
};
|
||||
};
|
||||
|
||||
export type RotateAlertTokenMutationVariables = Exact<{
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
export type RotateAlertTokenMutation = {
|
||||
rotateAlertToken: {
|
||||
enabled: boolean;
|
||||
webhookPath: string | null;
|
||||
typeId: string | null;
|
||||
priority: IssuePriority;
|
||||
};
|
||||
};
|
||||
|
||||
export type DisableAlertIntegrationMutationVariables = Exact<{
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
export type DisableAlertIntegrationMutation = {
|
||||
disableAlertIntegration: boolean;
|
||||
};
|
||||
|
||||
export type AutomationFieldsFragment = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -3123,6 +3184,28 @@ export type TestWebhookMutation = {
|
||||
};
|
||||
};
|
||||
|
||||
export const AlertIntegrationFieldsFragmentDoc = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'AlertIntegrationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'AlertIntegration' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'typeId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'priority' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<AlertIntegrationFieldsFragment, unknown>;
|
||||
export const UserRefFieldsFragmentDoc = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
@@ -4459,6 +4542,286 @@ export const WebhookFieldsFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<WebhookFieldsFragment, unknown>;
|
||||
export const AlertIntegrationDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'AlertIntegration' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'alertIntegration' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'AlertIntegrationFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'AlertIntegrationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'AlertIntegration' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'typeId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'priority' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
AlertIntegrationQuery,
|
||||
AlertIntegrationQueryVariables
|
||||
>;
|
||||
export const SaveAlertIntegrationDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'SaveAlertIntegration' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'input' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'AlertSettingsInput' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'saveAlertIntegration' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'input' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'input' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'AlertIntegrationFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'AlertIntegrationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'AlertIntegration' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'typeId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'priority' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
SaveAlertIntegrationMutation,
|
||||
SaveAlertIntegrationMutationVariables
|
||||
>;
|
||||
export const RotateAlertTokenDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'RotateAlertToken' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'rotateAlertToken' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'AlertIntegrationFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'AlertIntegrationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'AlertIntegration' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'typeId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'priority' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
RotateAlertTokenMutation,
|
||||
RotateAlertTokenMutationVariables
|
||||
>;
|
||||
export const DisableAlertIntegrationDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'DisableAlertIntegration' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'disableAlertIntegration' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
DisableAlertIntegrationMutation,
|
||||
DisableAlertIntegrationMutationVariables
|
||||
>;
|
||||
export const AutomationsDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ReleasesGateway,
|
||||
SavedFiltersGateway,
|
||||
SprintsGateway,
|
||||
AlertsGateway,
|
||||
GitGateway,
|
||||
WebhooksGateway,
|
||||
TransferGateway,
|
||||
@@ -42,6 +43,7 @@ import { GraphqlRealtimeGateway } from './adapters/graphql-realtime.gateway';
|
||||
import { GraphqlSavedFiltersGateway } from './adapters/graphql-saved-filters.gateway';
|
||||
import { GraphqlSprintsGateway } from './adapters/graphql-sprints.gateway';
|
||||
import { GraphqlGitGateway } from './adapters/graphql-git.gateway';
|
||||
import { GraphqlAlertsGateway } from './adapters/graphql-alerts.gateway';
|
||||
import { GraphqlWebhooksGateway } from './adapters/graphql-webhooks.gateway';
|
||||
import { HttpTransferGateway } from './adapters/http-transfer.gateway';
|
||||
import { GraphqlAutomationsGateway } from './adapters/graphql-automations.gateway';
|
||||
@@ -78,6 +80,7 @@ export function provideTrackerPagesInfrastructure(): EnvironmentProviders {
|
||||
{ provide: SavedFiltersGateway, useClass: GraphqlSavedFiltersGateway },
|
||||
{ provide: SprintsGateway, useClass: GraphqlSprintsGateway },
|
||||
{ provide: GitGateway, useClass: GraphqlGitGateway },
|
||||
{ provide: AlertsGateway, useClass: GraphqlAlertsGateway },
|
||||
{ provide: WebhooksGateway, useClass: GraphqlWebhooksGateway },
|
||||
{ provide: TransferGateway, useClass: HttpTransferGateway },
|
||||
{ provide: AutomationsGateway, useClass: GraphqlAutomationsGateway },
|
||||
|
||||
Reference in New Issue
Block a user