Skip to content

Commit

Permalink
[Integration][Sentry] - Add Users and Teams (#1229)
Browse files Browse the repository at this point in the history
# Description

What - Added sentry users and teams

Why - For improved data model

How - Added sentryUser and sentryTeam blueprint. And added a relation
between issue and user

## Type of change

Please leave one option from the following and delete the rest:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] New Integration (non-breaking change which adds a new integration)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Non-breaking change (fix of existing functionality that will not
change current behavior)
- [ ] Documentation (added/updated documentation)

<h4> All tests should be run against the port production
environment(using a testing org). </h4>

### Core testing checklist

- [ ] Integration able to create all default resources from scratch
- [ ] Resync finishes successfully
- [ ] Resync able to create entities
- [ ] Resync able to update entities
- [ ] Resync able to detect and delete entities
- [ ] Scheduled resync able to abort existing resync and start a new one
- [ ] Tested with at least 2 integrations from scratch
- [ ] Tested with Kafka and Polling event listeners
- [ ] Tested deletion of entities that don't pass the selector


### Integration testing checklist

- [ ] Integration able to create all default resources from scratch
- [ ] Resync able to create entities
- [ ] Resync able to update entities
- [ ] Resync able to detect and delete entities
- [ ] Resync finishes successfully
- [ ] If new resource kind is added or updated in the integration, add
example raw data, mapping and expected result to the `examples` folder
in the integration directory.
- [ ] If resource kind is updated, run the integration with the example
data and check if the expected result is achieved
- [ ] If new resource kind is added or updated, validate that
live-events for that resource are working as expected
- [ ] Docs PR link [here](#)

### Preflight checklist

- [ ] Handled rate limiting
- [ ] Handled pagination
- [ ] Implemented the code in async
- [ ] Support Multi account

## Screenshots

Include screenshots from your environment showing how the resources of
the integration will look.

<img width="1208" alt="Screenshot 2024-12-12 at 7 40 42 PM"
src="https://github.com/user-attachments/assets/a25f67b3-6fa3-4e2b-a0bd-271ee4af5d62"
/>

<img width="1208" alt="Screenshot 2024-12-12 at 7 41 03 PM"
src="https://github.com/user-attachments/assets/3a8e90eb-31f1-4efe-acb0-6a3426727474"
/>

## API Documentation


[Users](https://docs.sentry.io/api/organizations/list-an-organizations-members/)
[Teams](https://docs.sentry.io/api/teams/list-an-organizations-teams/)
[Team Members](https://docs.sentry.io/api/teams/list-a-teams-members/)
  • Loading branch information
PeyGis authored Dec 18, 2024
1 parent 195849a commit 6b44ba5
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 10 deletions.
114 changes: 113 additions & 1 deletion integrations/sentry/.port/resources/blueprints.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,103 @@
[
{
"identifier": "sentryUser",
"description": "This blueprint represents a Sentry user in our software catalog.",
"title": "Sentry User",
"icon": "Sentry",
"schema": {
"properties": {
"username": {
"type": "string",
"title": "Username"
},
"isActive": {
"type": "boolean",
"title": "Is Active"
},
"dateJoined": {
"type": "string",
"format": "date-time",
"title": "Date Joined"
},
"lastLogin": {
"type": "string",
"format": "date-time",
"title": "Last Login"
},
"orgRole": {
"icon": "DefaultProperty",
"title": "Organization Role",
"type": "string",
"enum": [
"member",
"admin",
"owner",
"manager",
"biling"
],
"enumColors": {
"member": "pink",
"admin": "green",
"owner": "green",
"manager": "yellow",
"biling": "lightGray"
}
},
"inviteStatus": {
"type": "string",
"title": "Invite Status",
"icon": "DefaultProperty"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {}
},
{
"identifier": "sentryTeam",
"description": "This blueprint represents an Sentry team in our software catalog",
"title": "Sentry Team",
"icon": "Sentry",
"schema": {
"properties": {
"dateCreated": {
"type": "string",
"title": "Date Created",
"format": "date-time"
},
"memberCount": {
"type": "number",
"title": "Number of Members"
},
"roles": {
"type": "string",
"title": "Roles"
},
"projects": {
"items": {
"type": "string"
},
"type": "array",
"title": "Projects"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {
"members": {
"title": "Members",
"target": "sentryUser",
"required": false,
"many": true
}
}
},
{
"identifier": "sentryProject",
"title": "Sentry Project Environment",
Expand Down Expand Up @@ -34,7 +133,14 @@
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
"relations": {
"team": {
"title": "Team",
"target": "sentryTeam",
"required": false,
"many": false
}
}
},
{
"identifier": "sentryIssue",
Expand Down Expand Up @@ -78,6 +184,12 @@
"target": "sentryProject",
"required": false,
"many": true
},
"assignedTo": {
"title": "Assigned To",
"target": "sentryUser",
"required": false,
"many": false
}
}
}
Expand Down
42 changes: 41 additions & 1 deletion integrations/sentry/.port/resources/port-app-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
createMissingRelatedEntities: true
deleteDependentEntities: true
resources:
- kind: user
selector:
query: 'true'
port:
entity:
mappings:
identifier: .email
title: .user.name
blueprint: '"sentryUser"'
properties:
username: .user.username
isActive: .user.isActive
dateJoined: .user.dateJoined
lastLogin: .user.lastLogin
orgRole: .orgRole
inviteStatus: .inviteStatus
- kind: team
selector:
query: 'true'
includeMembers: true
port:
entity:
mappings:
identifier: .slug
title: .name
blueprint: '"sentryTeam"'
properties:
dateCreated: .dateCreated
memberCount: .memberCount
roles: .teamRole
projects: .projects | map (.slug)
relations:
members: if .__members != null then .__members | map(.user.email) | map(select(. != null)) else [] end
- kind: project-tag
selector:
query: "true"
Expand All @@ -16,7 +49,13 @@ resources:
platform: .platform
status: .status
link: .organization.links.organizationUrl + "/projects/" + .name

relations:
team:
combinator: '"and"'
rules:
- property: '"projects"'
operator: '"contains"'
value: .slug
- kind: issue-tag
selector:
query: "true"
Expand All @@ -33,3 +72,4 @@ resources:
isUnhandled: .isUnhandled
relations:
projectEnvironment: '[(.project.slug as $slug | .__tags[] | "\($slug)-\(.name)")]'
assignedTo: .assignedTo.email
7 changes: 5 additions & 2 deletions integrations/sentry/.port/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ features:
- kind: issue
- kind: project-tag
- kind: issue-tag
- kind: user
- kind: team
configurations:
- name: sentryHost
required: true
type: url
description: "Sentry host URL"
default: "https://sentry.io"
- name: sentryToken
required: true
type: string
description: "Sentry token"
sensitive: true
type: string
description: The Sentry API token used for authentication. To create a token, see the <a href="https://docs.sentry.io/api/auth/" target="_blank"> Sentry documentation</a>
- name: sentryOrganization
required: true
type: string
Expand Down
9 changes: 9 additions & 0 deletions integrations/sentry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- towncrier release notes start -->

## 0.1.101 (2024-12-17)


### Improvements

- Added support for Sentry users and teams and established a relationship between the two resources
- Updated issues blueprint to include an assignedTo relation


## 0.1.100 (2024-12-15)


Expand Down
31 changes: 31 additions & 0 deletions integrations/sentry/clients/sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,34 @@ async def get_projects_tags_from_projects(
if project_tags_batch:
project_tags.append(project_tags_batch)
return flatten_list(project_tags)

async def get_paginated_teams(
self,
) -> AsyncGenerator[list[dict[str, Any]], None]:
async for teams in self._get_paginated_resource(
f"{self.api_url}/organizations/{self.organization}/teams/"
):
yield teams

async def get_paginated_users(
self,
) -> AsyncGenerator[list[dict[str, Any]], None]:
async for users in self._get_paginated_resource(
f"{self.api_url}/organizations/{self.organization}/members/"
):
yield users

async def get_team_members(self, team_slug: str) -> list[dict[str, Any]]:
logger.info(f"Getting team members for team {team_slug}")
team_members_List = []
async for team_members_batch in self._get_paginated_resource(
f"{self.api_url}/teams/{self.organization}/{team_slug}/members/"
):
logger.info(
f"Received a batch of {len(team_members_batch)} members for team {team_slug}"
)
team_members_List.extend(team_members_batch)
logger.info(
f"Received a total of {len(team_members_List)} members for team {team_slug}"
)
return team_members_List
20 changes: 19 additions & 1 deletion integrations/sentry/integration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from pydantic.fields import Field
from typing import Literal


from port_ocean.core.handlers.port_app_config.api import APIPortAppConfig
from port_ocean.core.handlers.port_app_config.models import (
Expand All @@ -17,12 +19,28 @@ class SentrySelector(Selector):
)


class TeamSelector(Selector):
include_members: bool = Field(
alias="includeMembers",
default=False,
description="Whether to include the members of the team, defaults to false",
)


class SentryResourceConfig(ResourceConfig):
selector: SentrySelector
kind: Literal["project", "issue", "project-tag", "issue-tag"]


class TeamResourceConfig(ResourceConfig):
kind: Literal["team"]
selector: TeamSelector


class SentryPortAppConfig(PortAppConfig):
resources: list[SentryResourceConfig] = Field(default_factory=list) # type: ignore
resources: list[SentryResourceConfig | TeamResourceConfig | ResourceConfig] = Field(
default_factory=list
)


class SentryIntegration(BaseIntegration):
Expand Down
49 changes: 45 additions & 4 deletions integrations/sentry/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from enum import StrEnum
from typing import Any, cast
import asyncio
from loguru import logger

from port_ocean.context.ocean import ocean
from clients.sentry import SentryClient
from port_ocean.context.event import event
from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE

from loguru import logger

from port_ocean.utils.async_iterators import stream_async_iterators_tasks

from integration import TeamResourceConfig
from clients.sentry import SentryClient


class ObjectKind(StrEnum):
PROJECT = "project"
ISSUE = "issue"
PROJECT_TAG = "project-tag"
ISSUE_TAG = "issue-tag"
USER = "user"
TEAM = "team"


def init_client() -> SentryClient:
Expand All @@ -25,6 +30,42 @@ def init_client() -> SentryClient:
return sentry_client


async def enrich_team_with_members(
sentry_client: SentryClient,
team_batch: list[dict[str, Any]],
) -> list[dict[str, Any]]:
team_tasks = [sentry_client.get_team_members(team["slug"]) for team in team_batch]
results = await asyncio.gather(*team_tasks)

for team, members in zip(team_batch, results):
team["__members"] = members

return team_batch


@ocean.on_resync(ObjectKind.USER)
async def on_resync_user(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
sentry_client = init_client()
async for users in sentry_client.get_paginated_users():
logger.info(f"Received {len(users)} users")
yield users


@ocean.on_resync(ObjectKind.TEAM)
async def on_resync_team(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
sentry_client = init_client()
selector = cast(TeamResourceConfig, event.resource_config).selector
async for team_batch in sentry_client.get_paginated_teams():
logger.info(f"Received {len(team_batch)} teams")
if selector.include_members:
team_with_members = await enrich_team_with_members(
sentry_client, team_batch
)
yield team_with_members
else:
yield team_batch


@ocean.on_resync(ObjectKind.PROJECT)
async def on_resync_project(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
sentry_client = init_client()
Expand Down
2 changes: 1 addition & 1 deletion integrations/sentry/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "sentry"
version = "0.1.100"
version = "0.1.101"
description = "Sentry Integration"
authors = ["Dvir Segev <[email protected]>","Matan Geva <[email protected]>"]

Expand Down

0 comments on commit 6b44ba5

Please sign in to comment.