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:
2026-10-17 12:36:55 +02:00
parent 42a21412d9
commit 034704e46f
34 changed files with 3248 additions and 6 deletions

View File

@@ -10912,6 +10912,453 @@
"security": []
}
},
"/api/projects/{project}/alert-integration": {
"get": {
"operationId": "AlertsController_integration",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AlertIntegrationDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Whether alerts open bugs, and the secret URL (for admins)",
"tags": [
"Alerts"
]
},
"put": {
"description": "The URL stays the same; rotate it with POST …/token.",
"operationId": "AlertsController_save",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AlertSettingsDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AlertIntegrationDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Turn alerts → bugs on, or change what it opens (owners, admins)",
"tags": [
"Alerts"
]
},
"delete": {
"operationId": "AlertsController_disable",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Turn it off; the bugs it opened stay",
"tags": [
"Alerts"
]
}
},
"/api/projects/{project}/alert-integration/token": {
"post": {
"operationId": "AlertsController_rotate",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AlertIntegrationDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "A new secret URL; the old one stops working at once",
"tags": [
"Alerts"
]
}
},
"/api/webhooks/alerts/{project}/{token}": {
"post": {
"description": "Understands Sentry (internal integration or webhooks plugin), Uptime Kuma, Prometheus Alertmanager and Grafana; anything else sends JSON with `title`, and optionally `fingerprint`, `description`, `url` and `resolved`. A new problem opens a bug as TaskStream; the same one again goes to that bug while it is open (a comment at most every 30 minutes); «resolved» is said on it.",
"operationId": "AlertsController_receive",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
},
{
"name": "token",
"required": true,
"in": "path",
"description": "The secret of the URL",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"title": "La cola de correos está atascada",
"fingerprint": "mail-queue",
"description": "1.200 mensajes esperando",
"url": "https://grafana.example.com/d/mail"
}
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AlertOutcomeDto"
}
}
}
},
"403": {
"description": "Unknown project, alerts off or bad token"
}
},
"summary": "Receive an alert (called by the monitoring tool)",
"tags": [
"Alerts"
],
"security": []
}
},
"/api/webhook-events": {
"get": {
"operationId": "WebhooksController_events",
@@ -17252,6 +17699,79 @@
"recorded"
]
},
"AlertIntegrationDto": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"webhookPath": {
"type": "string",
"nullable": true,
"description": "Path of the URL on this API, secret token included; only owners and admins see it",
"example": "/api/webhooks/alerts/0b4c…/9f2e…"
},
"typeId": {
"type": "string",
"nullable": true,
"description": "Ticket type it opens; null: the project's first bug type"
},
"priority": {
"allOf": [
{
"$ref": "#/components/schemas/IssuePriority"
}
]
}
},
"required": [
"enabled",
"webhookPath",
"typeId",
"priority"
]
},
"AlertSettingsDto": {
"type": "object",
"properties": {
"typeId": {
"type": "string",
"nullable": true,
"description": "Ticket type to open; null: the project's first bug type"
},
"priority": {
"allOf": [
{
"$ref": "#/components/schemas/IssuePriority"
}
]
}
}
},
"AlertOutcomeDto": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"created",
"repeated",
"resolved",
"ignored"
],
"description": "created: a new bug; repeated: it went to the open bug; resolved: said on it; ignored: not an alert, or over with nothing open"
},
"issueKey": {
"type": "string",
"nullable": true,
"example": "WEB-42"
}
},
"required": [
"action",
"issueKey"
]
},
"WebhookDeliveryDto": {
"type": "object",
"properties": {

View File

@@ -45,6 +45,27 @@ type Aging {
thresholdDays: Float
}
"""Alerts → bugs: the secret URL monitoring tools call to open bugs"""
type AlertIntegration {
enabled: Boolean!
priority: IssuePriority!
"""Ticket type it opens; null: the project's first bug type"""
typeId: ID
"""
Path of the URL on this API, secret token included; only owners and admins see it
"""
webhookPath: String
}
input AlertSettingsInput {
priority: IssuePriority
"""Ticket type to open; null: the project's first bug type"""
typeId: ID
}
type AreaOption {
archived: Boolean!
id: String!
@@ -977,6 +998,9 @@ type Mutation {
deleteSprint(sprintId: ID!): CommandResult!
deleteWebhook(webhookId: ID!): Boolean!
"""Turn it off; the bugs it opened stay"""
disableAlertIntegration(projectId: ID!): Boolean!
"""
A new ticket like this one: what it says, not where it stands (the key comes in the result)
"""
@@ -1029,9 +1053,17 @@ type Mutation {
"""My calendar URL stops working"""
revokeCalendarFeed: Boolean!
"""A new secret URL; the old one stops working at once"""
rotateAlertToken(projectId: ID!): AlertIntegration!
"""A new secret for a repository; the old one stops working"""
rotateGitRepositorySecret(projectId: ID!, repositoryId: ID!): GitRepository!
"""
Turn alerts → bugs on, or change the type and priority it opens (the URL stays)
"""
saveAlertIntegration(input: AlertSettingsInput!, projectId: ID!): AlertIntegration!
"""Save the current board filters under a name (members of the project)"""
saveFilter(input: SaveFilterInput!): ID!
@@ -1507,6 +1539,7 @@ type Query {
limit: Int! = 50
projectId: ID!
): [Activity!]!
alertIntegration(projectId: ID!): AlertIntegration!
automations(projectId: ID!): [Automation!]!
"""The project's link for clients, if any (owners and admins)"""

View File

@@ -1679,6 +1679,65 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/projects/{project}/alert-integration": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Whether alerts open bugs, and the secret URL (for admins) */
get: operations["AlertsController_integration"];
/**
* Turn alerts → bugs on, or change what it opens (owners, admins)
* @description The URL stays the same; rotate it with POST …/token.
*/
put: operations["AlertsController_save"];
post?: never;
/** Turn it off; the bugs it opened stay */
delete: operations["AlertsController_disable"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/projects/{project}/alert-integration/token": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** A new secret URL; the old one stops working at once */
post: operations["AlertsController_rotate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/webhooks/alerts/{project}/{token}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Receive an alert (called by the monitoring tool)
* @description Understands Sentry (internal integration or webhooks plugin), Uptime Kuma, Prometheus Alertmanager and Grafana; anything else sends JSON with `title`, and optionally `fingerprint`, `description`, `url` and `resolved`. A new problem opens a bug as TaskStream; the same one again goes to that bug while it is open (a comment at most every 30 minutes); «resolved» is said on it.
*/
post: operations["AlertsController_receive"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/webhook-events": {
parameters: {
query?: never;
@@ -3534,6 +3593,31 @@ export interface components {
/** @description Tickets that got something new */
recorded: number;
};
AlertIntegrationDto: {
enabled: boolean;
/**
* @description Path of the URL on this API, secret token included; only owners and admins see it
* @example /api/webhooks/alerts/0b4c…/9f2e…
*/
webhookPath: string | null;
/** @description Ticket type it opens; null: the project's first bug type */
typeId: string | null;
priority: components["schemas"]["IssuePriority"];
};
AlertSettingsDto: {
/** @description Ticket type to open; null: the project's first bug type */
typeId?: string | null;
priority?: components["schemas"]["IssuePriority"];
};
AlertOutcomeDto: {
/**
* @description created: a new bug; repeated: it went to the open bug; resolved: said on it; ignored: not an alert, or over with nothing open
* @enum {string}
*/
action: "created" | "repeated" | "resolved" | "ignored";
/** @example WEB-42 */
issueKey: string | null;
};
WebhookDeliveryDto: {
/** Format: date-time */
at: string;
@@ -11249,6 +11333,329 @@ export interface operations {
};
};
};
AlertsController_integration: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AlertIntegrationDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
AlertsController_save: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["AlertSettingsDto"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AlertIntegrationDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
AlertsController_disable: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
AlertsController_rotate: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AlertIntegrationDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
AlertsController_receive: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
/** @description The secret of the URL */
token: string;
};
cookie?: never;
};
requestBody?: {
content: {
"application/json": Record<string, never>;
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AlertOutcomeDto"];
};
};
/** @description Unknown project, alerts off or bad token */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
WebhooksController_events: {
parameters: {
query?: never;