Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webhook and header types #294

Open
NatoBoram opened this issue Dec 14, 2023 · 10 comments
Open

Webhook and header types #294

NatoBoram opened this issue Dec 14, 2023 · 10 comments
Assignees

Comments

@NatoBoram
Copy link

NatoBoram commented Dec 14, 2023

I realize this might be out of scope for this library, but I would like for this library to expose types for Jira's webhooks and the headers that come with it.

For example, here's a Jira headers, taken from https://developer.atlassian.com/cloud/jira/software/webhooks/

export interface JiraHeaders {
	/** Every webhook contains the `X-Atlassian-Webhook-Identifier` header that
	 * provides an identifier for a webhook. This identifier is unique within a
	 * Jira Cloud tenant and is the same across retries. After you have processed
	 * a webhook, you can use the identifier to filter out retries. */
	readonly "X-Atlassian-Webhook-Identifier": string
	/** The `X-Atlassian-Webhook-Retry` header with the current retry count is
	 * included with webhooks that have been retried. Monitor this header and
	 * cross-reference it with the callback server logs to stay on top of any
	 * unexpected reliability problems. */
	readonly "X-Atlassian-Webhook-Retry": number
	/** Each webhook contains `X-Atlassian-Webhook-Flow` header with `"Primary"`
	 * or `"Secondary"` value.
	 *
	 * All Primary webhooks should be delivered within 30 seconds.
	 *
	 * All Secondary webhooks are a result of long-lasting bulk or cascade
	 * operation (bulk issue update, project deletion, issue deletion etc.).
	 * those webhooks, the expected delivery time requirements are relaxed,
	 * the delivery should take no more than 15 minutes.
	 *
	 * Note, in those cases, the top level webhook is transferred via Primary
	 * flow, and all dependent webhooks are transferred using the Secondary flow.
	 * For example, when deleting an issue, the issue_deleted event is transferred
	 * as Primary but all dependent `commend_deleted`, `attachment_deleted`
	 * `issuelink_deleted` etc. are Secondary. */
	readonly "X-Atlassian-Webhook-Flow": "Primary" | "Secondary"
	/** To trace the origin of a webhook, Connect apps can attach the additional
	 * `X-Atlassian-Webhook-Trace` HTTP header with any value consisting of a
	 * string of up to 1024 printable ASCII characters to a REST API request.
	 *
	 * The header and its value are then attached to every webhook sent from Jira for the REST API request.
	 *
	 * The app can use the webhook trace header to, for example:
	 *
	 * * Differentiate between webhooks triggered by various REST API requests.
	 * * Track a webhook’s delivery.
	 * * Attach other data for use when a webhook arrives.
	 */
	readonly "X-Atlassian-Webhook-Trace": string
}
@MrRefactoring MrRefactoring self-assigned this Jan 3, 2024
@MrRefactoring
Copy link
Owner

Hello @NatoBoram! Could you please define which exact endpoints should to have these headers?

@NatoBoram
Copy link
Author

If you go to https://webhook.site then create a WebHook in Jira at https://${USER}.atlassian.net/plugins/servlet/webhooks pointing to your custom listener, you can receive these headers.

For example, I made a test board with a test issue and I received these headers:

connection: close
accept-encoding: gzip,deflate
user-agent: Atlassian Webhook HTTP Client
host: webhook.site
content-type: application/json; charset=UTF-8
content-length: 7157
x-b3-sampled: 0
x-b3-spanid: 6349452bd099e495
x-b3-traceid: a4ecf355b72258e87c3b2215d1628c5c
accept: */*
x-atlassian-webhook-flow: Primary
x-atlassian-webhook-identifier: 69312270414585119

I also got this request body:

{
	"timestamp": 1704303069063,
	"webhookEvent": "jira:issue_created",
	"issue_event_type_name": "issue_created",
	"user": {
		"self": "https://natoboram.atlassian.net/rest/api/2/user?accountId=5c19e3e3f9cb4734ae1b918e",
		"accountId": "5c19e3e3f9cb4734ae1b918e",
		"avatarUrls": {
			"48x48": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
			"24x24": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
			"16x16": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
			"32x32": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png"
		},
		"displayName": "Nato Boram",
		"active": true,
		"timeZone": "America/Toronto",
		"accountType": "atlassian"
	},
	"issue": {
		"id": "10000",
		"self": "https://natoboram.atlassian.net/rest/api/2/10000",
		"key": "TEST-1",
		"fields": {
			"statuscategorychangedate": "2024-01-03T12:31:09.209-0500",
			"issuetype": {
				"self": "https://natoboram.atlassian.net/rest/api/2/issuetype/10001",
				"id": "10001",
				"description": "Tasks track small, distinct pieces of work.",
				"iconUrl": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium",
				"name": "Task",
				"subtask": false,
				"avatarId": 10318,
				"entityId": "9baea660-ceab-4eaa-8046-9f53e3558981",
				"hierarchyLevel": 0
			},
			"timespent": null,
			"customfield_10030": null,
			"customfield_10031": null,
			"project": {
				"self": "https://natoboram.atlassian.net/rest/api/2/project/10000",
				"id": "10000",
				"key": "TEST",
				"name": "Test Project",
				"projectTypeKey": "software",
				"simplified": true,
				"avatarUrls": {
					"48x48": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10423",
					"24x24": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10423?size=small",
					"16x16": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10423?size=xsmall",
					"32x32": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10423?size=medium"
				}
			},
			"customfield_10032": null,
			"fixVersions": [],
			"aggregatetimespent": null,
			"resolution": null,
			"customfield_10027": null,
			"customfield_10028": null,
			"customfield_10029": null,
			"resolutiondate": null,
			"workratio": -1,
			"lastViewed": null,
			"watches": {
				"self": "https://natoboram.atlassian.net/rest/api/2/issue/TEST-1/watchers",
				"watchCount": 0,
				"isWatching": true
			},
			"issuerestriction": { "issuerestrictions": {}, "shouldDisplay": true },
			"created": "2024-01-03T12:31:08.939-0500",
			"customfield_10020": null,
			"customfield_10021": null,
			"customfield_10022": null,
			"customfield_10023": null,
			"priority": {
				"self": "https://natoboram.atlassian.net/rest/api/2/priority/3",
				"iconUrl": "https://natoboram.atlassian.net/images/icons/priorities/medium.svg",
				"name": "Medium",
				"id": "3"
			},
			"customfield_10024": null,
			"customfield_10025": null,
			"customfield_10026": null,
			"labels": [],
			"customfield_10016": null,
			"customfield_10017": null,
			"customfield_10018": {
				"hasEpicLinkFieldDependency": false,
				"showField": false,
				"nonEditableReason": {
					"reason": "PLUGIN_LICENSE_ERROR",
					"message": "The Parent Link is only available to Jira Premium users."
				}
			},
			"customfield_10019": "0|hzzzzz:",
			"aggregatetimeoriginalestimate": null,
			"timeestimate": null,
			"versions": [],
			"issuelinks": [],
			"assignee": null,
			"updated": "2024-01-03T12:31:08.939-0500",
			"status": {
				"self": "https://natoboram.atlassian.net/rest/api/2/status/10000",
				"description": "",
				"iconUrl": "https://natoboram.atlassian.net/",
				"name": "To Do",
				"id": "10000",
				"statusCategory": {
					"self": "https://natoboram.atlassian.net/rest/api/2/statuscategory/2",
					"id": 2,
					"key": "new",
					"colorName": "blue-gray",
					"name": "New"
				}
			},
			"components": [],
			"timeoriginalestimate": null,
			"description": null,
			"customfield_10010": null,
			"customfield_10014": null,
			"timetracking": {},
			"customfield_10015": null,
			"customfield_10005": null,
			"customfield_10006": null,
			"security": null,
			"customfield_10007": null,
			"customfield_10008": null,
			"aggregatetimeestimate": null,
			"attachment": [],
			"customfield_10009": null,
			"summary": "This is a new issue, created in Jira.",
			"creator": {
				"self": "https://natoboram.atlassian.net/rest/api/2/user?accountId=5c19e3e3f9cb4734ae1b918e",
				"accountId": "5c19e3e3f9cb4734ae1b918e",
				"avatarUrls": {
					"48x48": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
					"24x24": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
					"16x16": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
					"32x32": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png"
				},
				"displayName": "Nato Boram",
				"active": true,
				"timeZone": "America/Toronto",
				"accountType": "atlassian"
			},
			"subtasks": [],
			"reporter": {
				"self": "https://natoboram.atlassian.net/rest/api/2/user?accountId=5c19e3e3f9cb4734ae1b918e",
				"accountId": "5c19e3e3f9cb4734ae1b918e",
				"avatarUrls": {
					"48x48": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
					"24x24": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
					"16x16": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
					"32x32": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png"
				},
				"displayName": "Nato Boram",
				"active": true,
				"timeZone": "America/Toronto",
				"accountType": "atlassian"
			},
			"aggregateprogress": { "progress": 0, "total": 0 },
			"customfield_10001": null,
			"customfield_10002": null,
			"customfield_10003": null,
			"customfield_10004": null,
			"environment": null,
			"duedate": null,
			"progress": { "progress": 0, "total": 0 },
			"votes": {
				"self": "https://natoboram.atlassian.net/rest/api/2/issue/TEST-1/votes",
				"votes": 0,
				"hasVoted": false
			}
		}
	},
	"changelog": {
		"id": "10000",
		"items": [
			{
				"field": "priority",
				"fieldtype": "jira",
				"fieldId": "priority",
				"from": null,
				"fromString": null,
				"to": "3",
				"toString": "Medium"
			},
			{
				"field": "reporter",
				"fieldtype": "jira",
				"fieldId": "reporter",
				"from": null,
				"fromString": null,
				"to": "5c19e3e3f9cb4734ae1b918e",
				"toString": "Nato Boram",
				"tmpFromAccountId": null,
				"tmpToAccountId": "5c19e3e3f9cb4734ae1b918e"
			},
			{
				"field": "Status",
				"fieldtype": "jira",
				"fieldId": "status",
				"from": null,
				"fromString": null,
				"to": "10000",
				"toString": "To Do"
			},
			{
				"field": "summary",
				"fieldtype": "jira",
				"fieldId": "summary",
				"from": null,
				"fromString": null,
				"to": null,
				"toString": "This is a new issue, created in Jira."
			}
		]
	}
}

@MrRefactoring
Copy link
Owner

MrRefactoring commented Jan 3, 2024

Screenshot 2024-01-04 at 00 00 37
I see few endpoints. For which one I should to add headers?

https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-webhooks/#api-rest-api-3-webhook-get

@NatoBoram
Copy link
Author

It's not actually for an endpoint on Jira's side, it's for requests that the Jira server itself sends when you add webhooks at https://natoboram.atlassian.net/plugins/servlet/webhooks.

Example:

image

When you receive these events, it looks like this:

image

For example, I'm making a program that interacts with the API to add comments to issues. This can be done with Jira.js. However, this program needs to know when an issue is created, so I'm registering a webhook on Jira's side so Jira can tell me when there's a new issue. This would require additional types that I haven't seen in Jira.js.

@MrRefactoring
Copy link
Owner

Oh okay I catch it. You wanna extract headers from response

@NatoBoram
Copy link
Author

Yep, both headers and body for WebHook types

@MrRefactoring
Copy link
Owner

How you imagine library can handle this webhook response?

@NatoBoram
Copy link
Author

I don't think this library should "handle" them, but rather, expose the types for these. Something like this:

// Example enum for webhook events
const webhookEvents = {
	issue_created: "jira:issue_created",
	issue_updated: "jira:issue_updated",
} as const
type WebhookEvent = (typeof webhookEvents)[keyof typeof webhookEvents]

const issueEventTypeNames = {
	issue_created: "issue_created",
	issue_updated: "issue_updated",
} as const
type IssueEventTypeName =
	(typeof issueEventTypeNames)[keyof typeof issueEventTypeNames]

// Example interfaces for webhook bodies
interface JiraWebHookBase {
	readonly webhookEvent: WebhookEvent
	readonly issue_event_type_name: IssueEventTypeName
}

interface JiraWebHookIssueCreated extends JiraWebHookBase {
	readonly webhookEvent: typeof webhookEvents.issue_created
	readonly issue_event_type_name: typeof issueEventTypeNames.issue_created
}

interface JiraWebHookIssueUpdated extends JiraWebHookBase {
	readonly webhookEvent: typeof webhookEvents.issue_updated
	readonly issue_event_type_name: typeof issueEventTypeNames.issue_updated
}

type JiraWebHook = JiraWebHookIssueCreated | JiraWebHookIssueUpdated

// Example headers for webhooks
interface JiraHeaders {
	readonly "x-atlassian-webhook-flow": "Primary"
}

Someone using this library would just have to import and use these types with whatever router they are using.

const router = express()
router.post("/jira", (req: Request<undefined, undefined, JiraWebHook>, res) => {
	switch (req.body.webhookEvent) {
		case webhookEvents.issue_created:
			// `req.body` is now narrowed to `JiraWebHookIssueCreated`
			console.log("req.body", req.body)
			return res.sendStatus(200)

		case webhookEvents.issue_updated:
			// `req.body` is now narrowed to `JiraWebHookIssueUpdated`
			console.log("req.body", req.body)
			return res.sendStatus(200)

		default:
			// `req.body` is now narrowed to `never`
			console.log("req.body", req.body)
			return res.sendStatus(400)
	}
})

@cenobitedk
Copy link

Hi @MrRefactoring and @NatoBoram
I would like this as well. Currently there's no way to tell what props are available on the incoming webhook body.
This library is already great for using the API (thanks @MrRefactoring 🙏) and it would make sense to have it in the same package. Besides I haven't found any other package that provides the types.

There haven't been any comments on this since January, but the issue is still open. Is it still a feature request, or something you are actually working on? :-)

@PhilDakin
Copy link

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants