Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@nestjs/websockets": "^10.3.1",
"@prisma/client": "^5.17.0",
"@prisma/instrumentation": "^5.17.0",
"@tegonhq/sdk": "workspace:^",
"@tegonhq/types": "workspace:*",
"@tiptap/core": "^2.3.0",
"@tiptap/extension-blockquote": "^2.4.0",
Expand Down
71 changes: 71 additions & 0 deletions apps/server/src/integrations/sentry/account-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { PrismaClient } from '@prisma/client';
import { createIntegrationAccount } from 'integrations/utils';
import { IntegrationEventPayload } from '@tegonhq/types';
import { SentryCallbackBody, SentryTokenResponseData } from './types';
import axios from 'axios';

const prisma = new PrismaClient();

export const integrationCreate = async (
userId: string,
workspaceId: string,
data: IntegrationEventPayload,
) => {
const { oauthResponse, integrationDefinition } = data;

const { code, installationId, orgSlug } = oauthResponse as SentryCallbackBody;

const requestData = {
grant_type: 'authorization_code',
code,
client_id: process.env.SENTRY_CLIENT_ID,
client_secret: process.env.SENTRY_CLIENT_SECRET,
};

// get access and refresh token
const tokenResponse: { data: SentryTokenResponseData } = await axios.post(
`https://sentry.io/api/0/sentry-app-installations/${installationId}/authorizations/`,
requestData,
);

// set active status of installation
const verificationResponse = await axios.put(
`https://sentry.io/api/0/sentry-app-installations/${installationId}/`,
{ status: 'installed' },
{
headers: {
Authorization: `Bearer ${tokenResponse.data.token}`,
},
},
);

const appSlug = verificationResponse.data.app.slug;

const integrationConfiguration = {
accessToken: tokenResponse.data.token,
refreshToken: tokenResponse.data.refreshToken,
};

const accountId = oauthResponse.installationId as string;

const settings = {
code: oauthResponse.code,
};

// Update the integration account with the new configuration in the database
const integrationAccount = await createIntegrationAccount(prisma, {
settings,
userId,
accountId,
config: integrationConfiguration,
workspaceId,
integrationDefinitionId: integrationDefinition.id,
});

return {
status: true,
message: `Created integration account ${integrationAccount.id}`,
orgSlug,
appSlug,
};
};
30 changes: 30 additions & 0 deletions apps/server/src/integrations/sentry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
IntegrationEventPayload,
IntegrationPayloadEventType,
} from '@tegonhq/types';
import { webhookResponse } from './webhook-response';
import { isActionSupportedEvent } from './is_action_supported_event';
import { integrationCreate } from './account-create';

export default async function run(eventPayload: IntegrationEventPayload) {
switch (eventPayload.event) {
// Used to save settings data
case IntegrationPayloadEventType.CREATE:
return await integrationCreate(
eventPayload.userId,
eventPayload.workspaceId,
eventPayload.data,
);

case IntegrationPayloadEventType.WEBHOOK_RESPONSE:
return await webhookResponse(eventPayload.eventBody);

case IntegrationPayloadEventType.IS_ACTION_SUPPORTED_EVENT:
return isActionSupportedEvent(eventPayload.eventBody);

default:
return {
message: `The event payload type is ${eventPayload.event}`,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const SUPPORTED_TYPES = ['assigned'];

interface Payload {
action: string;
}

export function isActionSupportedEvent(payload: Payload) {
return SUPPORTED_TYPES.includes(payload.action);
}
48 changes: 48 additions & 0 deletions apps/server/src/integrations/sentry/sentry-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"elements": [
{
"type": "issue-link",
"link": {
"uri": "/v1/webhook/sentry?action=link-issue",
"required_fields": [
{
"type": "text",
"label": "Task Name",
"name": "title",
"default": "issue.title"
},
{
"type": "select",
"label": "Select Issue Id To Link with",
"name": "issueId",
"uri": "/v1/webhook/sentry/get-issues/",
"skip_load_on_open": true
}
]
},
"create": {
"uri": "/v1/webhook/sentry?action=create-issue",
"required_fields": [
{
"type": "text",
"label": "Task Name",
"name": "title",
"default": "issue.title"
},
{
"type": "textarea",
"label": "Task Description",
"name": "description",
"default": "issue.description"
},
{
"type": "select",
"label": "Which team would in which you like to link to create Issue?",
"name": "teamId",
"uri": "/v1/webhook/sentry/get-teams/"
}
]
}
}
]
}
13 changes: 13 additions & 0 deletions apps/server/src/integrations/sentry/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type SentryTokenResponseData = {
expiresAt: string; // ISO date string at which token must be refreshed
token: string; // Bearer token authorized to make Sentry API requests
refreshToken: string; // Refresh token required to get a new Bearer token after expiration
};

export interface SentryCallbackBody {
workspaceId: string;
integrationDefinitionId: string;
installationId: string;
code: string;
orgSlug: string;
}
204 changes: 204 additions & 0 deletions apps/server/src/integrations/sentry/webhook-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { PrismaClient } from '@prisma/client';
import {
createIssue,
search,
setAccessToken,
createLinkedIssue,
} from '@tegonhq/sdk';

const prisma = new PrismaClient();

export const webhookResponse = async (eventBody: any) => {
if (eventBody.action === 'created') {
return {
grant_type: 'authorization_code',
code: eventBody.data.installation.code,
};
}

if (eventBody.action === 'get-teams') {
return getAllTeams(eventBody.installationId);
}

if (eventBody.action === 'create-issue') {
return createTegonIssue(eventBody);
}

if (eventBody.action === 'get-issues') {
return getTegonIssues(eventBody);
}

if (eventBody.action === 'link-issue') {
return createTegonLinkedIssue(eventBody);
}

return true;
};

async function getAllTeams(accountId: string): Promise<any> {
const integrationAccount = await prisma.integrationAccount.findFirst({
where: { accountId, deleted: null },
include: { workspace: true, integrationDefinition: true },
});

if (!integrationAccount) {
return null;
}

const workspaceId = integrationAccount.workspaceId;
const teams = await prisma.team.findMany({
where: { workspaceId },
select: { id: true, name: true },
});

return teams.map(({ name, id }) => ({ label: name, value: id }));
}

async function createTegonIssue(eventBody: any): Promise<any> {
const {
installationId: accountId,
fields,
webUrl,
project,
issueId,
actor,
} = eventBody;
const { teamId, title, description } = fields;

const integrationAccount = await prisma.integrationAccount.findFirst({
where: { accountId, deleted: null },
include: { workspace: true, integrationDefinition: true },
});

if (!integrationAccount) {
return null;
}

const personalAccessToken = await prisma.personalAccessToken.findFirst({
where: { workspaceId: integrationAccount.workspaceId, deleted: null },
Comment thread
mrchocha marked this conversation as resolved.
Outdated
});

if (!personalAccessToken) {
return null;
}

const workflow = await prisma.workflow.findFirst({
where: { teamId, category: 'BACKLOG', deleted: null },
});

setAccessToken(personalAccessToken.jwt);

const createdIssueResp = await createIssue({
teamId,
title,
description,
stateId: workflow.id,
linkIssueData: {
url: webUrl,
sourceId: integrationAccount.integrationDefinitionId,
sourceData: {
title,
issueId,
project,
actor,
},
},
});

const response = {
webUrl: `http://localhost:3000/${integrationAccount.workspace.name}/issue/${createdIssueResp.number}`,
project: createdIssueResp.team.name,
identifier: `${createdIssueResp.team.identifier}-${createdIssueResp.number}`,
};

return response;
}

async function getTegonIssues(eventBody: any): Promise<any> {
const { installationId: accountId, query } = eventBody;
const integrationAccount = await prisma.integrationAccount.findFirst({
where: { accountId, deleted: null },
include: { workspace: true, integrationDefinition: true },
});

if (!integrationAccount) {
return null;
}

const personalAccessToken = await prisma.personalAccessToken.findFirst({
where: { workspaceId: integrationAccount.workspaceId, deleted: null },
Comment thread
mrchocha marked this conversation as resolved.
Outdated
});

if (!personalAccessToken) {
return null;
}

// Add search API here.
setAccessToken(personalAccessToken.jwt);

const searchIssueResp = (await search({
workspaceId: personalAccessToken.workspaceId,
query,
})) as any[];

return searchIssueResp.map(({ title, id, issueNumber }) => ({
label: `${issueNumber}: ${title}`,
value: id,
}));
}

async function createTegonLinkedIssue(eventBody: any): Promise<any> {
const {
installationId: accountId,
fields,
webUrl,
project,
actor,
} = eventBody;
const { issueId, title } = fields;

const integrationAccount = await prisma.integrationAccount.findFirst({
where: { accountId, deleted: null },
include: { workspace: true, integrationDefinition: true },
});

if (!integrationAccount) {
return null;
}

const personalAccessToken = await prisma.personalAccessToken.findFirst({
where: { workspaceId: integrationAccount.workspaceId, deleted: null },
Comment thread
mrchocha marked this conversation as resolved.
Outdated
});

if (!personalAccessToken) {
return null;
}

// Add search API here.
setAccessToken(personalAccessToken.jwt);
const issue = await prisma.issue.findFirst({
where: { id: issueId, deleted: null },
include: { team: true },
});

const teamId = issue.teamId;

await createLinkedIssue({
issueId,
teamId,
url: webUrl,
title,
sourceId: integrationAccount.integrationDefinitionId,
sourceData: {
title,
issueId,
project,
actor,
},
});
return {
webUrl: `http://localhost:3000/${integrationAccount.workspace.name}/issue/${issue.number}`,
project: issue.team.name,
identifier: `${issue.team.identifier}-${issue.number}`,
};
}
Loading