You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by ab...@apache.org on 2023/11/24 06:17:29 UTC

(incubator-devlake) branch main updated: feat: Opsgenie plugin implementation (#6154)

This is an automated email from the ASF dual-hosted git repository.

abeizn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 4453aa51d feat: Opsgenie plugin implementation (#6154)
4453aa51d is described below

commit 4453aa51de5f64259d68290b74dba992db5186da
Author: Vitor Ferraz Sandes <62...@users.noreply.github.com>
AuthorDate: Fri Nov 24 03:17:24 2023 -0300

    feat: Opsgenie plugin implementation (#6154)
    
    * feat: Opsgenie plugin implementation
    
    * fix(lint): lint with gofmt
    
    * refactor(lint): removing unused config-ui files
    
    * feat(task): changed incident collector to accept incremental collection
    
    * fix(config-ui): adjust the params for bp add connection (#6241)
    
    * refactor: removed unused code (#6243)
    
    * refactor(config-ui): remove content about user info (#6245)
    
    * fix(azure): fix 400 error when cloing azure repos (#6246)
    
    * fix: normalize_bp_settings throws error (#6249)
    
    * fix: normalize_bp_settings throws error
    
    * fix: pg ci
    
    * fix: pg ci (#6252)
    
    * fix(gitlab): fix incremental deployment collector (#6254)
    
    * fix(gitlab): fix incremental deployment collector
    
    * fix(gitlab): rollback to full collector
    
    * refactor(gitlab): remove unused codes
    
    * fix(gitlab): collect deployment incrementally
    
    * fix: modify _tool_jira_issue_relationships primary key (#6253)
    
    * fix: modify _tool_jira_issue_relationships primary key
    
    * fix: ci lint
    
    * feat(pagerduty): search remote scope by keyword (#6255)
    
    * feat(pagerduty): search remote scope by keyword
    
    * feat(pagerduty): add `createdAt` field
    
    * refactor(impl): refactoring impl.go and service model
    
    * refactor(comment): removing comment from incident collector
    
    * feat(task): added GetCreated function
    
    * feat(zentao): check db url when testing connections (#6258)
    
    * feat(zentao): check db url when testing connections
    
    * fix(zentao): remove test codes
    
    * fix(zentao): remove test codes
    
    * fix(configui): tagsLimit should be a number (#6261)
    
    * fix(check): fixing lint in config-ui
    
    * refactor(newline): adding removed newline
    
    * fix(config-ui): removed old plugin type
    
    * refactor(config-ui): remove type inheritance from opsgenieconfig
    
    * style(config-ui): update icon with icon.svg
    
    ---------
    
    Co-authored-by: 青湛 <0x...@gmail.com>
    Co-authored-by: Klesh Wong <zh...@merico.dev>
    Co-authored-by: Lynwee <li...@merico.dev>
    Co-authored-by: abeizn <zi...@merico.dev>
---
 backend/plugins/opsgenie/api/blueprint_v200.go     |  119 ++
 backend/plugins/opsgenie/api/connection.go         |  153 ++
 backend/plugins/opsgenie/api/init.go               |   59 +
 backend/plugins/opsgenie/api/remote.go             |  211 +++
 backend/plugins/opsgenie/api/scope.go              |  111 ++
 backend/plugins/opsgenie/api/swagger.go            |   32 +
 backend/plugins/opsgenie/e2e/incident_test.go      |  143 ++
 .../e2e/raw_tables/_raw_opsgenie_incidents.csv     |    3 +
 .../e2e/raw_tables/_raw_opsgenie_teams.csv         |    3 +
 .../e2e/raw_tables/_raw_opsgenie_users.csv         |    2 +
 .../snapshot_tables/_tool_opsgenie_assignments.csv |    5 +
 .../snapshot_tables/_tool_opsgenie_incidents.csv   |    3 +
 .../snapshot_tables/_tool_opsgenie_responders.csv  |    3 +
 .../e2e/snapshot_tables/_tool_opsgenie_teams.csv   |    3 +
 .../e2e/snapshot_tables/_tool_opsgenie_users.csv   |    2 +
 .../e2e/snapshot_tables/issue_assignees.csv        |    5 +
 .../opsgenie/e2e/snapshot_tables/issues.csv        |    3 +
 .../plugins/opsgenie/e2e/snapshot_tables/teams.csv |    3 +
 .../plugins/opsgenie/e2e/snapshot_tables/users.csv |    2 +
 backend/plugins/opsgenie/impl/impl.go              |  189 +++
 backend/plugins/opsgenie/models/assignment.go      |   33 +
 backend/plugins/opsgenie/models/connection.go      |   64 +
 backend/plugins/opsgenie/models/incident.go        |   55 +
 .../migrationscripts/20221115_add_init_tables.go   |   49 +
 .../20230123_add_connection_fields.go              |   51 +
 .../20230203_add_endpoint_proxy_to_conn.go         |   50 +
 .../20230203_add_transformation_rules.go           |   50 +
 .../migrationscripts/20230531_scope_config.go      |   56 +
 .../20230605_remove_scope_config.go                |   39 +
 .../20230630_add_raw_param_table_for_scopes.go     |   81 ++
 .../models/migrationscripts/archived/assignment.go |   33 +
 .../models/migrationscripts/archived/connection.go |   32 +
 .../models/migrationscripts/archived/incident.go   |   44 +
 .../models/migrationscripts/archived/responder.go  |   33 +
 .../models/migrationscripts/archived/service.go    |   37 +
 .../models/migrationscripts/archived/team.go       |   32 +
 .../archived/transformation_rules.go               |   32 +
 .../models/migrationscripts/archived/user.go       |   32 +
 .../opsgenie/models/migrationscripts/register.go   |   35 +
 backend/plugins/opsgenie/models/raw/incident.go    |   45 +
 backend/plugins/opsgenie/models/raw/service.go     |   31 +
 backend/plugins/opsgenie/models/raw/team.go        |   28 +
 backend/plugins/opsgenie/models/raw/user.go        |   42 +
 backend/plugins/opsgenie/models/responder.go       |   35 +
 backend/plugins/opsgenie/models/scope_config.go    |   24 +
 backend/plugins/opsgenie/models/service.go         |   69 +
 backend/plugins/opsgenie/models/team.go            |   34 +
 backend/plugins/opsgenie/models/user.go            |   34 +
 backend/plugins/opsgenie/opsgenie.go               |   38 +
 .../plugins/opsgenie/tasks/incidents_collector.go  |  136 ++
 .../plugins/opsgenie/tasks/incidents_converter.go  |  168 +++
 .../plugins/opsgenie/tasks/incidents_extractor.go  |   95 ++
 .../plugins/opsgenie/tasks/service_converter.go    |   83 ++
 backend/plugins/opsgenie/tasks/shared.go           |   25 +
 backend/plugins/opsgenie/tasks/task_data.go        |   88 ++
 backend/plugins/opsgenie/tasks/teams_collector.go  |   69 +
 backend/plugins/opsgenie/tasks/teams_convertor.go  |   82 ++
 backend/plugins/opsgenie/tasks/teams_extractor.go  |   70 +
 backend/plugins/opsgenie/tasks/users_collector.go  |   80 ++
 backend/plugins/opsgenie/tasks/users_convertor.go  |   82 ++
 backend/plugins/opsgenie/tasks/users_extractor.go  |   70 +
 backend/plugins/table_info_test.go                 |    2 +
 config-ui/src/plugins/register/index.ts            |    2 +
 .../src/plugins/register/opsgenie/assets/icon.svg  |   28 +
 config-ui/src/plugins/register/opsgenie/config.tsx |   62 +
 .../opsgenie/connection-fields/endpoint.tsx        |   52 +
 .../register/opsgenie/connection-fields/index.ts   |   19 +
 .../register/opsgenie/connection-fields/styled.ts  |   72 +
 config-ui/src/plugins/register/opsgenie/index.ts   |   19 +
 grafana/dashboards/Opsgenie.json                   | 1454 ++++++++++++++++++++
 70 files changed, 5030 insertions(+)

diff --git a/backend/plugins/opsgenie/api/blueprint_v200.go b/backend/plugins/opsgenie/api/blueprint_v200.go
new file mode 100644
index 000000000..d2b53b2fa
--- /dev/null
+++ b/backend/plugins/opsgenie/api/blueprint_v200.go
@@ -0,0 +1,119 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"github.com/apache/incubator-devlake/core/errors"
+	coreModels "github.com/apache/incubator-devlake/core/models"
+	"github.com/apache/incubator-devlake/core/models/domainlayer"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/core/utils"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/tasks"
+)
+
+func MakeDataSourcePipelinePlanV200(
+	subtaskMetas []plugin.SubTaskMeta,
+	connectionId uint64,
+	bpScopes []*coreModels.BlueprintScope,
+) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
+	// get the connection info for url
+	connection := &models.OpsgenieConnection{}
+	err := connectionHelper.FirstById(connection, connectionId)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	plan := make(coreModels.PipelinePlan, len(bpScopes))
+	plan, err = makeDataSourcePipelinePlanV200(subtaskMetas, plan, bpScopes, connection)
+	if err != nil {
+		return nil, nil, err
+	}
+	scopes, err := makeScopesV200(bpScopes, connection)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return plan, scopes, nil
+}
+
+func makeDataSourcePipelinePlanV200(
+	subtaskMetas []plugin.SubTaskMeta,
+	plan coreModels.PipelinePlan,
+	bpScopes []*coreModels.BlueprintScope,
+	connection *models.OpsgenieConnection,
+) (coreModels.PipelinePlan, errors.Error) {
+	for i, bpScope := range bpScopes {
+		// get board and scope config from db
+		service, scopeConfig, err := scopeHelper.DbHelper().GetScopeAndConfig(connection.ID, bpScope.ScopeId)
+		if err != nil {
+			return nil, err
+		}
+		// construct task options for opsgenie
+		op := &tasks.OpsgenieOptions{
+			ConnectionId: service.ConnectionId,
+			ServiceId:    service.Id,
+			ServiceName:  service.Name,
+		}
+
+		var options map[string]any
+		options, err = tasks.EncodeTaskOptions(op)
+		if err != nil {
+			return nil, err
+		}
+		var subtasks []string
+		subtasks, err = api.MakePipelinePlanSubtasks(subtaskMetas, scopeConfig.Entities)
+		if err != nil {
+			return nil, err
+		}
+		stage := []*coreModels.PipelineTask{
+			{
+				Plugin:   "opsgenie",
+				Subtasks: subtasks,
+				Options:  options,
+			},
+		}
+		plan[i] = stage
+	}
+	return plan, nil
+}
+
+func makeScopesV200(bpScopes []*coreModels.BlueprintScope, connection *models.OpsgenieConnection) ([]plugin.Scope, errors.Error) {
+	scopes := make([]plugin.Scope, 0)
+	for _, bpScope := range bpScopes {
+		// get board and scope config from db
+		service, scopeConfig, err := scopeHelper.DbHelper().GetScopeAndConfig(connection.ID, bpScope.ScopeId)
+		if err != nil {
+			return nil, err
+		}
+		// add board to scopes
+		if utils.StringsContains(scopeConfig.Entities, plugin.DOMAIN_TYPE_TICKET) {
+			scopeTicket := &ticket.Board{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: didgen.NewDomainIdGenerator(&models.Service{}).Generate(connection.ID, service.Id),
+				},
+				Name: service.Name,
+			}
+			scopes = append(scopes, scopeTicket)
+		}
+	}
+	return scopes, nil
+}
diff --git a/backend/plugins/opsgenie/api/connection.go b/backend/plugins/opsgenie/api/connection.go
new file mode 100644
index 000000000..50ea2eb78
--- /dev/null
+++ b/backend/plugins/opsgenie/api/connection.go
@@ -0,0 +1,153 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+)
+
+// @Summary test opsgenie connection
+// @Description Test Opsgenie Connection
+// @Tags plugins/opsgenie
+// @Param body body models.OpsgenieConn true "json body"
+// @Success 200  {object} shared.ApiBody "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/opsgenie/test [POST]
+func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	var connection models.OpsgenieConn
+
+	err := api.Decode(input.Body, &connection, vld)
+	if err != nil {
+		return nil, err
+	}
+
+	apiClient, err := api.NewApiClientFromConnection(context.TODO(), basicRes, &connection)
+	if err != nil {
+		return nil, err
+	}
+
+	// check API permissions
+	response, err := apiClient.Get("v2/heartbeats/HeartbeatName/ping", nil, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if response.StatusCode == http.StatusUnauthorized {
+		return nil, errors.HttpStatus(http.StatusUnauthorized).New("StatusUnauthorized error when testing api or read_api permissions")
+	}
+
+	if response.StatusCode == http.StatusUnprocessableEntity {
+		return nil, errors.HttpStatus(http.StatusUnprocessableEntity).New("StatusUnprocessableEntity error when testing api")
+	}
+
+	if response.StatusCode == http.StatusForbidden {
+		return nil, errors.HttpStatus(http.StatusForbidden).New("API Key need 'Read' and 'Configuration access' Access rights")
+	}
+
+	if response.StatusCode == http.StatusOK {
+		return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
+	}
+
+	return &plugin.ApiResourceOutput{Body: nil, Status: response.StatusCode}, errors.HttpStatus(response.StatusCode).Wrap(err, "could not validate connection")
+}
+
+// @Summary create opsgenie connection
+// @Description Create Opsgenie connection
+// @Tags plugins/opsgenie
+// @Param body body models.OpsgenieConnection true "json body"
+// @Success 200  {object} models.OpsgenieConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/opsgenie/connections [POST]
+func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	connection := &models.OpsgenieConnection{}
+	err := connectionHelper.Create(connection, input)
+	if err != nil {
+		return nil, err
+	}
+	return &plugin.ApiResourceOutput{Body: connection, Status: http.StatusOK}, nil
+}
+
+// @Summary patch opsgenie connection
+// @Description Patch Opsgenie connection
+// @Tags plugins/opsgenie
+// @Param body body models.OpsgenieConnection true "json body"
+// @Success 200  {object} models.OpsgenieConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId} [PATCH]
+func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	connection := &models.OpsgenieConnection{}
+	err := connectionHelper.Patch(connection, input)
+	if err != nil {
+		return nil, err
+	}
+	return &plugin.ApiResourceOutput{Body: connection, Status: http.StatusOK}, nil
+}
+
+// @Summary delete opsgenie connection
+// @Description Delete Opsgenie connection
+// @Tags plugins/opsgenie
+// @Success 200  {object} models.OpsgenieConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 409  {object} services.BlueprintProjectPairs "References exist to this connection"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId} [DELETE]
+func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return connectionHelper.Delete(&models.OpsgenieConnection{}, input)
+}
+
+// @Summary list opsgenie connections
+// @Description List Opsgenie connections
+// @Tags plugins/opsgenie
+// @Success 200  {object} models.OpsgenieConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/opsgenie/connections [GET]
+func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	var connections []models.OpsgenieConnection
+	err := connectionHelper.List(&connections)
+	if err != nil {
+		return nil, err
+	}
+
+	return &plugin.ApiResourceOutput{Body: connections}, nil
+}
+
+// @Summary get opsgenie connection
+// @Description Get Opsgenie connection
+// @Tags plugins/opsgenie
+// @Success 200  {object} models.OpsgenieConnection
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId} [GET]
+func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	connection := &models.OpsgenieConnection{}
+	err := connectionHelper.First(connection, input.Params)
+	if err != nil {
+		return nil, err
+	}
+	return &plugin.ApiResourceOutput{Body: connection}, nil
+}
diff --git a/backend/plugins/opsgenie/api/init.go b/backend/plugins/opsgenie/api/init.go
new file mode 100644
index 000000000..3b1e85c89
--- /dev/null
+++ b/backend/plugins/opsgenie/api/init.go
@@ -0,0 +1,59 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+	"github.com/go-playground/validator/v10"
+)
+
+var vld *validator.Validate
+var connectionHelper *api.ConnectionApiHelper
+
+var scopeHelper *api.ScopeApiHelper[models.OpsgenieConnection, models.Service, models.OpsenieScopeConfig]
+
+var basicRes context.BasicRes
+
+func Init(br context.BasicRes, p plugin.PluginMeta) {
+
+	basicRes = br
+	vld = validator.New()
+	connectionHelper = api.NewConnectionHelper(
+		basicRes,
+		vld,
+		p.Name(),
+	)
+	params := &api.ReflectionParameters{
+		ScopeIdFieldName:     "Id",
+		ScopeIdColumnName:    "id",
+		RawScopeParamName:    "ScopeId",
+		SearchScopeParamName: "name",
+	}
+	scopeHelper = api.NewScopeHelper[models.OpsgenieConnection, models.Service, models.OpsenieScopeConfig](
+		basicRes,
+		vld,
+		connectionHelper,
+		api.NewScopeDatabaseHelperImpl[models.OpsgenieConnection, models.Service, models.OpsenieScopeConfig](
+			basicRes, connectionHelper, params),
+		params,
+		&api.ScopeHelperOptions{},
+	)
+}
diff --git a/backend/plugins/opsgenie/api/remote.go b/backend/plugins/opsgenie/api/remote.go
new file mode 100644
index 000000000..b6b1780b4
--- /dev/null
+++ b/backend/plugins/opsgenie/api/remote.go
@@ -0,0 +1,211 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/models/common"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models/raw"
+)
+
+type RemoteScopesChild struct {
+	Type     string      `json:"type"`
+	ParentId *string     `json:"parentId"`
+	Id       string      `json:"id"`
+	Name     string      `json:"name"`
+	FullName string      `json:"fullName"`
+	Data     interface{} `json:"data"`
+}
+
+type RemoteScopesOutput struct {
+	Children      []RemoteScopesChild `json:"children"`
+	NextPageToken string              `json:"nextPageToken"`
+}
+
+type SearchRemoteScopesOutput struct {
+	Children []RemoteScopesChild `json:"children"`
+	Page     int                 `json:"page"`
+	PageSize int                 `json:"pageSize"`
+}
+
+type PageData struct {
+	Page    int `json:"page"`
+	PerPage int `json:"per_page"`
+}
+
+type ServiceResponse struct {
+	TotalCount int           `json:"totalCount"`
+	Data       []raw.Service `json:"data"`
+}
+
+const RemoteScopesPerPage int = 100
+const TypeScope string = "scope"
+
+// RemoteScopes list all available scopes (services) for this connection
+// @Summary list all available scopes (services) for this connection
+// @Description list all available scopes (services) for this connection
+// @Tags plugins/opsgenie
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param groupId query string false "group ID"
+// @Param pageToken query string false "page Token"
+// @Success 200  {object} RemoteScopesOutput
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId}/remote-scopes [GET]
+func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	connectionId, _ := extractParam(input.Params)
+	if connectionId == 0 {
+		return nil, errors.BadInput.New("invalid connectionId")
+	}
+
+	connection := &models.OpsgenieConnection{}
+	err := connectionHelper.First(connection, input.Params)
+	if err != nil {
+		return nil, err
+	}
+
+	pageToken, ok := input.Query["pageToken"]
+	if !ok || len(pageToken) == 0 {
+		pageToken = []string{""}
+	}
+
+	pageData, err := DecodeFromPageToken(pageToken[0])
+	if err != nil {
+		return nil, errors.BadInput.New("failed to get page token")
+	}
+
+	// create api client
+	apiClient, err := api.NewApiClientFromConnection(context.TODO(), basicRes, connection)
+	if err != nil {
+		return nil, err
+	}
+
+	query, err := GetQueryFromPageData(pageData)
+	if err != nil {
+		return nil, err
+	}
+
+	var res *http.Response
+	outputBody := &RemoteScopesOutput{}
+	res, err = apiClient.Get("v1/services", query, nil)
+	if err != nil {
+		return nil, err
+	}
+	response := &ServiceResponse{}
+	err = api.UnmarshalResponse(res, response)
+	if err != nil {
+		return nil, err
+	}
+
+	// append service to output
+	for _, service := range response.Data {
+		child := RemoteScopesChild{
+			Type:     TypeScope,
+			Id:       service.Id,
+			Name:     service.Name,
+			FullName: service.Name,
+			Data: models.Service{
+				Url:    service.Links.Web,
+				Id:     service.Id,
+				Name:   service.Name,
+				TeamId: service.TeamId,
+				Scope: common.Scope{
+					NoPKModel:    common.NoPKModel{},
+					ConnectionId: connection.ID,
+				},
+			},
+		}
+		outputBody.Children = append(outputBody.Children, child)
+	}
+
+	return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil
+}
+
+// SearchRemoteScopes use the Search API and only return project
+// @Summary use the Search API and only return project
+// @Description use the Search API and only return project
+// @Tags plugins/opsgenie
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param search query string false "search"
+// @Param page query int false "page number"
+// @Param pageSize query int false "page size per page"
+// @Success 200  {object} SearchRemoteScopesOutput
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId}/search-remote-scopes [GET]
+func SearchRemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	// Not supported
+	return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusMethodNotAllowed}, nil
+}
+
+func EncodeToPageToken(pageData *PageData) (string, errors.Error) {
+	// Marshal json
+	pageTokenDecode, err := json.Marshal(pageData)
+	if err != nil {
+		return "", errors.Default.Wrap(err, fmt.Sprintf("Marshal pageToken failed %+v", pageData))
+	}
+	// Encode pageToken Base64
+	return base64.StdEncoding.EncodeToString(pageTokenDecode), nil
+}
+
+func DecodeFromPageToken(pageToken string) (*PageData, errors.Error) {
+	if pageToken == "" {
+		return &PageData{
+			Page:    0,
+			PerPage: RemoteScopesPerPage,
+		}, nil
+	}
+	// Decode pageToken Base64
+	pageTokenDecode, err := base64.StdEncoding.DecodeString(pageToken)
+	if err != nil {
+		return nil, errors.Default.Wrap(err, fmt.Sprintf("decode pageToken failed %s", pageToken))
+	}
+	// Unmarshal json
+	pt := &PageData{}
+	err = json.Unmarshal(pageTokenDecode, pt)
+	if err != nil {
+		return nil, errors.Default.Wrap(err, fmt.Sprintf("json Unmarshal pageTokenDecode failed %s", pageTokenDecode))
+	}
+	return pt, nil
+}
+
+func GetQueryFromPageData(pageData *PageData) (url.Values, errors.Error) {
+	query := url.Values{}
+	query.Set("offset", fmt.Sprintf("%v", pageData.Page*pageData.PerPage))
+	query.Set("limit", fmt.Sprintf("%v", pageData.PerPage))
+	return query, nil
+}
+
+func extractParam(params map[string]string) (uint64, uint64) {
+	connectionId, _ := strconv.ParseUint(params["connectionId"], 10, 64)
+	serviceId, _ := strconv.ParseUint(params["serviceId"], 10, 64)
+	return connectionId, serviceId
+}
diff --git a/backend/plugins/opsgenie/api/scope.go b/backend/plugins/opsgenie/api/scope.go
new file mode 100644
index 000000000..071d6d361
--- /dev/null
+++ b/backend/plugins/opsgenie/api/scope.go
@@ -0,0 +1,111 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+)
+
+type ScopeRes struct {
+	models.Service
+	api.ScopeResDoc[any]
+}
+
+type ScopeReq api.ScopeReq[models.Service]
+
+// PutScope create or update opsgenie service
+// @Summary create or update opsgenie service
+// @Description Create or update opsgenie service
+// @Tags plugins/opsgenie
+// @Accept application/json
+// @Param connectionId path int true "connection ID"
+// @Param scope body ScopeReq true "json"
+// @Success 200  {object} []ScopeRes
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId}/scopes [PUT]
+func PutScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Put(input)
+}
+
+// UpdateScope patch to opsgenie service
+// @Summary patch to opsgenie service
+// @Description patch to opsgenie service
+// @Tags plugins/opsgenie
+// @Accept application/json
+// @Param connectionId path int true "connection ID"
+// @Param serviceId path string true "service ID"
+// @Param scope body models.Service true "json"
+// @Success 200  {object} models.Service
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId}/scopes/{serviceId} [PATCH]
+func UpdateScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Update(input)
+}
+
+// GetScopeList get Opsgenie repos
+// @Summary get Opsgenie repos
+// @Description get Opsgenie repos
+// @Tags plugins/opsgenie
+// @Param connectionId path int true "connection ID"
+// @Param searchTerm query string false "search term for scope name"
+// @Param pageSize query int false "page size, default 50"
+// @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these scopes as part of the payload"
+// @Success 200  {object} []ScopeRes
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId}/scopes/ [GET]
+func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.GetScopeList(input)
+}
+
+// GetScope get one Opsgenie service
+// @Summary get one Opsgenie service
+// @Description get one Opsgenie service
+// @Tags plugins/opsgenie
+// @Param connectionId path int true "connection ID"
+// @Param serviceId path int true "service ID"
+// @Param blueprints query bool false "also return blueprints using this scope as part of the payload"
+// @Success 200  {object} ScopeRes
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId}/scopes/{serviceId} [GET]
+func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.GetScope(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the scope itself
+// @Summary delete plugin data associated with the scope and optionally the scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/opsgenie
+// @Param connectionId path int true "connection ID"
+// @Param serviceId path int true "service ID"
+// @Param delete_data_only query bool false "Only delete the scope data, not the scope itself"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} api.ScopeRefDoc "References exist to this scope"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/opsgenie/connections/{connectionId}/scopes/{serviceId} [DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	return scopeHelper.Delete(input)
+}
diff --git a/backend/plugins/opsgenie/api/swagger.go b/backend/plugins/opsgenie/api/swagger.go
new file mode 100644
index 000000000..8e34100a6
--- /dev/null
+++ b/backend/plugins/opsgenie/api/swagger.go
@@ -0,0 +1,32 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+	"github.com/apache/incubator-devlake/plugins/opsgenie/tasks"
+)
+
+type OpsgenieTaskOptions tasks.OpsgenieOptions
+
+// @Summary opsgenie task options for pipelines
+// @Description This is a dummy API to demonstrate the available task options for opsgenie pipelines
+// @Tags plugins/opsgenie
+// @Accept application/json
+// @Param pipeline body OpsgenieTaskOptions true "json"
+// @Router /pipelines/opsgenie/pipeline-task [post]
+func _() {}
diff --git a/backend/plugins/opsgenie/e2e/incident_test.go b/backend/plugins/opsgenie/e2e/incident_test.go
new file mode 100644
index 000000000..be189e98e
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/incident_test.go
@@ -0,0 +1,143 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/apache/incubator-devlake/core/models/common"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/crossdomain"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/helpers/e2ehelper"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/impl"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/tasks"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIncidentDataFlow(t *testing.T) {
+	var plugin impl.Opsgenie
+	dataflowTester := e2ehelper.NewDataFlowTester(t, "opsgenie", plugin)
+	options := tasks.OpsgenieOptions{
+		ConnectionId: 1,
+		ServiceId:    "695bce3d-4621-4630-8ae1-24eb89c22d6e",
+		ServiceName:  "TestService",
+		Tasks:        nil,
+	}
+	taskData := &tasks.OpsgenieTaskData{
+		Options: &options,
+	}
+
+	dataflowTester.FlushTabler(&models.Service{})
+
+	service := models.Service{
+		Url:  fmt.Sprintf("https://sandesvitor.app.opsgenie.com/service/%s", options.ServiceId),
+		Id:   options.ServiceId,
+		Name: options.ServiceName,
+	}
+	// scope
+	require.NoError(t, dataflowTester.Dal.CreateOrUpdate(&service))
+
+	// import raw data table
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_opsgenie_incidents.csv", "_raw_opsgenie_incidents")
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_opsgenie_users.csv", "_raw_opsgenie_users")
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_opsgenie_teams.csv", "_raw_opsgenie_teams")
+
+	dataflowTester.FlushTabler(&models.User{})
+	dataflowTester.FlushTabler(&models.Team{})
+	dataflowTester.FlushTabler(&models.Incident{})
+	dataflowTester.FlushTabler(&models.Responder{})
+	dataflowTester.FlushTabler(&models.Assignment{})
+
+	dataflowTester.FlushTabler(crossdomain.User{})
+	dataflowTester.FlushTabler(crossdomain.Team{})
+	dataflowTester.Subtask(tasks.ExtractUsersMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(
+		models.User{},
+		e2ehelper.TableOptions{
+			CSVRelPath:  "./snapshot_tables/_tool_opsgenie_users.csv",
+			IgnoreTypes: []any{common.Model{}},
+		},
+	)
+	dataflowTester.Subtask(tasks.ConvertUsersMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(crossdomain.User{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/users.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+
+	dataflowTester.Subtask(tasks.ExtractTeamsMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(
+		models.Team{},
+		e2ehelper.TableOptions{
+			CSVRelPath:  "./snapshot_tables/_tool_opsgenie_teams.csv",
+			IgnoreTypes: []any{common.Model{}},
+		},
+	)
+	dataflowTester.Subtask(tasks.ConvertTeamsMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(crossdomain.Team{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/teams.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+
+	dataflowTester.Subtask(tasks.ExtractIncidentsMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(
+		models.Incident{},
+		e2ehelper.TableOptions{
+			CSVRelPath:  "./snapshot_tables/_tool_opsgenie_incidents.csv",
+			IgnoreTypes: []any{common.Model{}},
+		},
+	)
+	dataflowTester.VerifyTableWithOptions(
+		models.Responder{},
+		e2ehelper.TableOptions{
+			CSVRelPath:  "./snapshot_tables/_tool_opsgenie_responders.csv",
+			IgnoreTypes: []any{common.Model{}},
+		},
+	)
+	dataflowTester.VerifyTableWithOptions(
+		models.Assignment{},
+		e2ehelper.TableOptions{
+			CSVRelPath:  "./snapshot_tables/_tool_opsgenie_assignments.csv",
+			IgnoreTypes: []any{common.Model{}},
+		},
+	)
+	dataflowTester.FlushTabler(&ticket.Issue{})
+	dataflowTester.FlushTabler(&ticket.IssueAssignee{})
+	dataflowTester.Subtask(tasks.ConvertIncidentsMeta, taskData)
+	dataflowTester.VerifyTableWithOptions(
+		ticket.Issue{},
+		e2ehelper.TableOptions{
+			CSVRelPath:  "./snapshot_tables/issues.csv",
+			IgnoreTypes: []any{common.NoPKModel{}},
+			IgnoreFields: []string{
+				"creator_id",
+				"creator_name",
+				"assignee_id",
+				"assignee_name",
+				"severity",
+				"component",
+				"original_project",
+			},
+		},
+	)
+	dataflowTester.VerifyTableWithOptions(ticket.IssueAssignee{}, e2ehelper.TableOptions{
+		CSVRelPath:  "./snapshot_tables/issue_assignees.csv",
+		IgnoreTypes: []interface{}{common.NoPKModel{}},
+	})
+}
diff --git a/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_incidents.csv b/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_incidents.csv
new file mode 100644
index 000000000..3ae6b9914
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_incidents.csv
@@ -0,0 +1,3 @@
+id,params,data,url,input,created_at
+8,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}","{""id"":""3a74524a-f492-4172-b3f5-6041c7cb404a"",""description"":""Test Description"",""impactedServices"":[""695bce3d-4621-4630-8ae1-24eb89c22d6e""],""tinyId"":""3"",""message"":""Incident#1"",""status"":""open"",""tags"":[],""createdAt"":""2023-09-06T14:30:56.346Z"",""updatedAt"":""2023-09-06T14:30:57.035Z"",""priority"":""P3"",""ownerTeam"":"""",""responders"":[{""type"":""team"",""id"":""c0259fb8-2068-48c5 [...]
+9,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}","{""id"":""3f84e009-7548-4e7d-832a-fa82c1ceb6b1"",""description"":""Test Description"",""impactedServices"":[""695bce3d-4621-4630-8ae1-24eb89c22d6e""],""tinyId"":""1"",""message"":""Incident#2"",""status"":""resolved"",""tags"":[],""createdAt"":""2023-09-05T18:20:28.003Z"",""updatedAt"":""2023-09-05T18:21:21.49Z"",""priority"":""P5"",""ownerTeam"":"""",""responders"":[{""type"":""team"",""id"":""c0259fb8-2068-4 [...]
diff --git a/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_teams.csv b/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_teams.csv
new file mode 100644
index 000000000..94c9ec0fb
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_teams.csv
@@ -0,0 +1,3 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}","{""id"":""6a95a94a-2db4-4ec0-9f0f-694c40e017fe"",""name"":""old_team"",""description"":"""",""links"":{""web"":""https://app.opsgenie.com/teams/dashboard/6a95a94a-2db4-4ec0-9f0f-694c40e017fe/main"",""api"":""https://api.opsgenie.com/v2/teams/6a95a94a-2db4-4ec0-9f0f-694c40e017fe""}}",https://api.opsgenie.com/v2/teams,null,2023-09-19 20:00:05.044
+2,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}","{""id"":""d2d47ba1-a034-488f-b1f4-be2cd3c693ad"",""name"":""new_team"",""description"":"""",""links"":{""web"":""https://app.opsgenie.com/teams/dashboard/d2d47ba1-a034-488f-b1f4-be2cd3c693ad/main"",""api"":""https://api.opsgenie.com/v2/teams/d2d47ba1-a034-488f-b1f4-be2cd3c693ad""}}",https://api.opsgenie.com/v2/teams,null,2023-09-19 20:00:05.044
\ No newline at end of file
diff --git a/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_users.csv b/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_users.csv
new file mode 100644
index 000000000..f8db900ad
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/raw_tables/_raw_opsgenie_users.csv
@@ -0,0 +1,2 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}","{""blocked"":false,""verified"":true,""id"":""2bf71ba0-cb10-4c6d-95d3-07ff54dcc91a"",""username"":""pikekad209@ipnuc.com"",""fullName"":""Hello World"",""role"":{""id"":""Owner"",""name"":""Owner""},""timeZone"":""America/Sao_Paulo"",""locale"":""en_US"",""userAddress"":{""country"":"""",""state"":"""",""city"":"""",""line"":"""",""zipCode"":""""},""createdAt"":""2023-09-19T18:59:10.194Z""}",https://api.opsgen [...]
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_assignments.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_assignments.csv
new file mode 100644
index 000000000..fdd79a459
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_assignments.csv
@@ -0,0 +1,5 @@
+created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,connection_id,responder_id,incident_id
+2023-09-15 18:51:47.974,2023-09-15 18:51:47.974,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,8,,1,b3948a43-fee8-4adc-b7bd-3ac58919f4eb,3a74524a-f492-4172-b3f5-6041c7cb404a
+2023-09-15 18:51:47.974,2023-09-15 18:51:47.974,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,9,,1,b3948a43-fee8-4adc-b7bd-3ac58919f4eb,3f84e009-7548-4e7d-832a-fa82c1ceb6b1
+2023-09-15 18:51:47.974,2023-09-15 18:51:47.974,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,8,,1,c0259fb8-2068-48c5-ae09-1b116e3579b1,3a74524a-f492-4172-b3f5-6041c7cb404a
+2023-09-15 18:51:47.974,2023-09-15 18:51:47.974,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,9,,1,c0259fb8-2068-48c5-ae09-1b116e3579b1,3f84e009-7548-4e7d-832a-fa82c1ceb6b1
\ No newline at end of file
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_incidents.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_incidents.csv
new file mode 100644
index 000000000..9e4d9749b
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_incidents.csv
@@ -0,0 +1,3 @@
+created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,connection_id,id,url,service_id,service_name,description,message,owner_team,priority,status,created_date,updated_date
+2023-09-15 18:51:47.961,2023-09-15 18:51:47.961,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,8,,1,3a74524a-f492-4172-b3f5-6041c7cb404a,https://sandesvitor.app.opsgenie.com/incident/detail/3a74524a-f492-4172-b3f5-6041c7cb404a,695bce3d-4621-4630-8ae1-24eb89c22d6e,TestService,Test Description,Incident#1,,P3,open,2023-09-06T14:30:56.346+00:00,2023-09-06T14:30:57.035+00:00
+2023-09-15 18:51:47.961,2023-09-15 18:51:47.961,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,9,,1,3f84e009-7548-4e7d-832a-fa82c1ceb6b1,https://sandesvitor.app.opsgenie.com/incident/detail/3f84e009-7548-4e7d-832a-fa82c1ceb6b1,695bce3d-4621-4630-8ae1-24eb89c22d6e,TestService,Test Description,Incident#2,,P5,resolved,2023-09-05T18:20:28.003+00:00,2023-09-05T18:21:21.490+00:00
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_responders.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_responders.csv
new file mode 100644
index 000000000..54dad5b76
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_responders.csv
@@ -0,0 +1,3 @@
+created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,connection_id,id,type
+2023-09-15 18:51:47.989,2023-09-15 18:51:47.989,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,9,,1,b3948a43-fee8-4adc-b7bd-3ac58919f4eb,user
+2023-09-15 18:51:47.989,2023-09-15 18:51:47.989,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,9,,1,c0259fb8-2068-48c5-ae09-1b116e3579b1,team
\ No newline at end of file
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_teams.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_teams.csv
new file mode 100644
index 000000000..eb2a748d8
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_teams.csv
@@ -0,0 +1,3 @@
+created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,connection_id,id,name,description
+2023-09-19 20:08:54.226,2023-09-19 20:08:54.226,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_teams,1,,1,6a95a94a-2db4-4ec0-9f0f-694c40e017fe,old_team,
+2023-09-19 20:08:54.226,2023-09-19 20:08:54.226,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_teams,2,,1,d2d47ba1-a034-488f-b1f4-be2cd3c693ad,new_team,
\ No newline at end of file
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_users.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_users.csv
new file mode 100644
index 000000000..2905d9724
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/_tool_opsgenie_users.csv
@@ -0,0 +1,2 @@
+created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,connection_id,id,username,full_name
+2023-09-19 20:08:54.209,2023-09-19 20:08:54.209,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_users,1,,1,2bf71ba0-cb10-4c6d-95d3-07ff54dcc91a,pikekad209@ipnuc.com,Hello World
\ No newline at end of file
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/issue_assignees.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/issue_assignees.csv
new file mode 100644
index 000000000..73f6efd71
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/issue_assignees.csv
@@ -0,0 +1,5 @@
+issue_id,assignee_id,assignee_name,created_at,created_at,_raw_data_params,_raw_data_params,_raw_data_id,_raw_data_remark
+opsgenie:Incident:1:3a74524a-f492-4172-b3f5-6041c7cb404a,b3948a43-fee8-4adc-b7bd-3ac58919f4eb,,2023-09-15 18:51:48.354,2023-09-15 18:51:48.354,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,8,
+opsgenie:Incident:1:3a74524a-f492-4172-b3f5-6041c7cb404a,c0259fb8-2068-48c5-ae09-1b116e3579b1,,2023-09-15 18:51:48.354,2023-09-15 18:51:48.354,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,8,
+opsgenie:Incident:1:3f84e009-7548-4e7d-832a-fa82c1ceb6b1,b3948a43-fee8-4adc-b7bd-3ac58919f4eb,,2023-09-15 18:51:48.354,2023-09-15 18:51:48.354,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,9,
+opsgenie:Incident:1:3f84e009-7548-4e7d-832a-fa82c1ceb6b1,c0259fb8-2068-48c5-ae09-1b116e3579b1,,2023-09-15 18:51:48.354,2023-09-15 18:51:48.354,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,9,
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/issues.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/issues.csv
new file mode 100644
index 000000000..a5d86f773
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/issues.csv
@@ -0,0 +1,3 @@
+id,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,url,icon_url,issue_key,title,description,epic_key,type,original_type,status,original_status,story_point,resolution_date,created_date,updated_date,lead_time_minutes,parent_issue_id,priority,original_estimate_minutes,time_spent_minutes,time_remaining_minutes,creator_id,creator_name,assignee_id,assignee_name,severity,component,original_project
+opsgenie:Incident:1:3a74524a-f492-4172-b3f5-6041c7cb404a,2023-09-15T18:51:48.360,2023-09-15T18:51:48.360,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,8,,https://sandesvitor.app.opsgenie.com/incident/detail/3a74524a-f492-4172-b3f5-6041c7cb404a,,3a74524a-f492-4172-b3f5-6041c7cb404a,Incident#1,Test Description,,INCIDENT,,IN_PROGRESS,open,0,,2023-09-06T14:30:56.346+00:00,2023-09-06T14:30:57.035+00:00,0,,P3,0,0,0,,,,,,,
+opsgenie:Incident:1:3f84e009-7548-4e7d-832a-fa82c1ceb6b1,2023-09-15T18:51:48.360,2023-09-15T18:51:48.360,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_incidents,9,,https://sandesvitor.app.opsgenie.com/incident/detail/3f84e009-7548-4e7d-832a-fa82c1ceb6b1,,3f84e009-7548-4e7d-832a-fa82c1ceb6b1,Incident#2,Test Description,,INCIDENT,,DONE,resolved,0,2023-09-05T18:21:21.490+00:00,2023-09-05T18:20:28.003+00:00,2023-09-05T18:21:21.490+00:00,0,,P5,0,0,0 [...]
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/teams.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/teams.csv
new file mode 100644
index 000000000..7e0c61d8e
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/teams.csv
@@ -0,0 +1,3 @@
+id,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,name,alias,parent_id,sorting_index
+opsgenie:Team:1:6a95a94a-2db4-4ec0-9f0f-694c40e017fe,2023-09-19 20:00:05.086,2023-09-19 20:00:13.442,"{""ConnectionId"":1,""ScopeId"":""f0f05045-ef9b-4108-85f5-af3dcac5eebd""}",_raw_opsgenie_teams,3,,6a95a94a-2db4-4ec0-9f0f-694c40e017fe,old_team,,0
+opsgenie:Team:1:d2d47ba1-a034-488f-b1f4-be2cd3c693ad,2023-09-19 20:00:05.086,2023-09-19 20:00:13.442,"{""ConnectionId"":1,""ScopeId"":""f0f05045-ef9b-4108-85f5-af3dcac5eebd""}",_raw_opsgenie_teams,4,,d2d47ba1-a034-488f-b1f4-be2cd3c693ad,new_team,,0
\ No newline at end of file
diff --git a/backend/plugins/opsgenie/e2e/snapshot_tables/users.csv b/backend/plugins/opsgenie/e2e/snapshot_tables/users.csv
new file mode 100644
index 000000000..4c6bb8bc3
--- /dev/null
+++ b/backend/plugins/opsgenie/e2e/snapshot_tables/users.csv
@@ -0,0 +1,2 @@
+id,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,email,name
+opsgenie:User:1:2bf71ba0-cb10-4c6d-95d3-07ff54dcc91a,2023-09-19 20:14:57.524,2023-09-19 20:14:57.524,"{""ConnectionId"":1,""ScopeId"":""695bce3d-4621-4630-8ae1-24eb89c22d6e""}",_raw_opsgenie_users,1,,pikekad209@ipnuc.com,Hello World
\ No newline at end of file
diff --git a/backend/plugins/opsgenie/impl/impl.go b/backend/plugins/opsgenie/impl/impl.go
new file mode 100644
index 000000000..ffb5114e4
--- /dev/null
+++ b/backend/plugins/opsgenie/impl/impl.go
@@ -0,0 +1,189 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package impl
+
+import (
+	"fmt"
+
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	coreModels "github.com/apache/incubator-devlake/core/models"
+	"github.com/apache/incubator-devlake/core/plugin"
+	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models/migrationscripts"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/tasks"
+)
+
+// make sure interface is implemented
+
+var _ interface {
+	plugin.PluginMeta
+	plugin.PluginInit
+	plugin.PluginModel
+	plugin.PluginMigration
+	plugin.PluginTask
+	plugin.PluginApi
+	plugin.DataSourcePluginBlueprintV200
+	plugin.CloseablePluginTask
+	plugin.PluginSource
+} = (*Opsgenie)(nil)
+
+type Opsgenie struct{}
+
+func (p Opsgenie) Init(basicRes context.BasicRes) errors.Error {
+	api.Init(basicRes, p)
+
+	return nil
+}
+
+func (p Opsgenie) Description() string {
+	return "collect some Opsgenie data"
+}
+
+// PkgPath information lost when compiled as plugin(.so)
+func (p Opsgenie) RootPkgPath() string {
+	return "github.com/apache/incubator-devlake/plugins/opsgenie"
+}
+
+func (p Opsgenie) Name() string {
+	return "opsgenie"
+}
+
+func (p Opsgenie) Scope() plugin.ToolLayerScope {
+	return &models.Service{}
+}
+
+func (p Opsgenie) ScopeConfig() dal.Tabler {
+	return nil
+}
+
+func (p Opsgenie) Connection() dal.Tabler {
+	return &models.OpsgenieConnection{}
+}
+
+func (p Opsgenie) GetTablesInfo() []dal.Tabler {
+	return []dal.Tabler{
+		&models.OpsgenieConnection{},
+		&models.Service{},
+		&models.Incident{},
+		&models.Responder{},
+		&models.Assignment{},
+		&models.User{},
+		&models.Team{},
+	}
+}
+
+func (p Opsgenie) SubTaskMetas() []plugin.SubTaskMeta {
+	return []plugin.SubTaskMeta{
+		tasks.CollectUsersMeta,
+		tasks.ExtractUsersMeta,
+		tasks.ConvertUsersMeta,
+		tasks.CollectTeamsMeta,
+		tasks.ExtractTeamsMeta,
+		tasks.ConvertTeamsMeta,
+		tasks.CollectIncidentsMeta,
+		tasks.ExtractIncidentsMeta,
+		tasks.ConvertIncidentsMeta,
+		tasks.ConvertServicesMeta,
+	}
+}
+
+func (p Opsgenie) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]interface{}) (interface{}, errors.Error) {
+	op, err := tasks.DecodeAndValidateTaskOptions(options)
+	if err != nil {
+		return nil, err
+	}
+	connectionHelper := helper.NewConnectionHelper(
+		taskCtx,
+		nil,
+		p.Name(),
+	)
+	connection := &models.OpsgenieConnection{}
+	err = connectionHelper.FirstById(connection, op.ConnectionId)
+	if err != nil {
+		return nil, errors.Default.Wrap(err, "unable to get Opsgenie connection by the given connection ID")
+	}
+
+	client, err := helper.NewApiClientFromConnection(taskCtx.GetContext(), taskCtx, connection)
+
+	if err != nil {
+		return nil, err
+	}
+	asyncClient, err := helper.CreateAsyncApiClient(taskCtx, client, nil)
+	if err != nil {
+		return nil, err
+	}
+	return &tasks.OpsgenieTaskData{
+		Options: op,
+		Client:  asyncClient,
+	}, nil
+}
+
+func (p Opsgenie) MigrationScripts() []plugin.MigrationScript {
+	return migrationscripts.All()
+}
+
+func (p Opsgenie) Close(taskCtx plugin.TaskContext) errors.Error {
+	_, ok := taskCtx.GetData().(*tasks.OpsgenieTaskData)
+	if !ok {
+		return errors.Default.New(fmt.Sprintf("GetData failed when try to close %+v", taskCtx))
+	}
+	return nil
+}
+
+func (p Opsgenie) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
+	return map[string]map[string]plugin.ApiResourceHandler{
+		"test": {
+			"POST": api.TestConnection,
+		},
+		"connections": {
+			"POST": api.PostConnections,
+			"GET":  api.ListConnections,
+		},
+		"connections/:connectionId": {
+			"GET":    api.GetConnection,
+			"PATCH":  api.PatchConnection,
+			"DELETE": api.DeleteConnection,
+		},
+		"connections/:connectionId/remote-scopes": {
+			"GET": api.RemoteScopes,
+		},
+		"connections/:connectionId/search-remote-scopes": {
+			"GET": api.SearchRemoteScopes,
+		},
+		"connections/:connectionId/scopes": {
+			"GET": api.GetScopeList,
+			"PUT": api.PutScope,
+		},
+		"connections/:connectionId/scopes/:scopeId": {
+			"GET":    api.GetScope,
+			"PATCH":  api.UpdateScope,
+			"DELETE": api.DeleteScope,
+		},
+	}
+}
+
+func (p Opsgenie) MakeDataSourcePipelinePlanV200(
+	connectionId uint64,
+	scopes []*coreModels.BlueprintScope,
+) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
+	return api.MakeDataSourcePipelinePlanV200(p.SubTaskMetas(), connectionId, scopes)
+}
diff --git a/backend/plugins/opsgenie/models/assignment.go b/backend/plugins/opsgenie/models/assignment.go
new file mode 100644
index 000000000..16ffbb66f
--- /dev/null
+++ b/backend/plugins/opsgenie/models/assignment.go
@@ -0,0 +1,33 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/core/models/common"
+)
+
+type Assignment struct {
+	common.NoPKModel
+	ConnectionId uint64
+	ResponderId  string `gorm:"primaryKey"`
+	IncidentId   string `gorm:"primaryKey"`
+}
+
+func (Assignment) TableName() string {
+	return "_tool_opsgenie_assignments"
+}
diff --git a/backend/plugins/opsgenie/models/connection.go b/backend/plugins/opsgenie/models/connection.go
new file mode 100644
index 000000000..69d1e6e30
--- /dev/null
+++ b/backend/plugins/opsgenie/models/connection.go
@@ -0,0 +1,64 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+// AccessToken implements HTTP Token Authentication with Access Token
+type OpsgenieGenieKey helper.AccessToken
+
+// SetupAuthentication sets up the request headers for authentication
+func (gk *OpsgenieGenieKey) SetupAuthentication(request *http.Request) errors.Error {
+	request.Header.Set("Authorization", fmt.Sprintf("GenieKey %s", gk.Token))
+	return nil
+}
+
+// OpsgenieConn holds the essential information to connect to the Opsgenie API
+type OpsgenieConn struct {
+	helper.RestConnection `mapstructure:",squash"`
+	OpsgenieGenieKey      `mapstructure:",squash"`
+}
+
+// OpsgenieConnection holds OpsgenieConn plus ID/Name for database storage
+type OpsgenieConnection struct {
+	helper.BaseConnection `mapstructure:",squash"`
+	OpsgenieConn          `mapstructure:",squash"`
+}
+
+// This object conforms to what the frontend currently expects.
+type OpsgenieResponse struct {
+	Name string `json:"name"`
+	ID   int    `json:"id"`
+	OpsgenieConnection
+}
+
+// Using User because it requires authentication.
+type ApiUserResponse struct {
+	Id   int
+	Name string `json:"name"`
+}
+
+func (OpsgenieConnection) TableName() string {
+	return "_tool_opsgenie_connections"
+}
diff --git a/backend/plugins/opsgenie/models/incident.go b/backend/plugins/opsgenie/models/incident.go
new file mode 100644
index 000000000..822469a37
--- /dev/null
+++ b/backend/plugins/opsgenie/models/incident.go
@@ -0,0 +1,55 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"time"
+
+	"github.com/apache/incubator-devlake/core/models/common"
+)
+
+const (
+	IncidentStatusClosed   IncidentStatus = "closed"
+	IncidentStatusOpen     IncidentStatus = "open"
+	IncidentStatusResolved IncidentStatus = "resolved"
+)
+
+type (
+	IncidentPriority string
+	IncidentStatus   string
+
+	Incident struct {
+		common.NoPKModel
+		ConnectionId uint64 `gorm:"primaryKey"`
+		Id           string `gorm:"primaryKey"`
+		Url          string
+		ServiceId    string
+		ServiceName  string
+		Description  string
+		Message      string
+		OwnerTeam    string
+		Priority     IncidentPriority
+		Status       IncidentStatus
+		CreatedDate  time.Time
+		UpdatedDate  time.Time
+	}
+)
+
+func (Incident) TableName() string {
+	return "_tool_opsgenie_incidents"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/20221115_add_init_tables.go b/backend/plugins/opsgenie/models/migrationscripts/20221115_add_init_tables.go
new file mode 100644
index 000000000..d76794ab6
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/20221115_add_init_tables.go
@@ -0,0 +1,49 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models/migrationscripts/archived"
+)
+
+type addInitTables struct{}
+
+func (*addInitTables) Up(baseRes context.BasicRes) errors.Error {
+	err := migrationhelper.AutoMigrateTables(
+		baseRes,
+		&archived.OpsgenieConnection{},
+		&archived.Service{},
+		&archived.Incident{},
+		&archived.Responder{},
+		&archived.Assignment{},
+		&archived.User{},
+		&archived.Team{},
+	)
+	return err
+}
+
+func (*addInitTables) Version() uint64 {
+	return 20221115000001
+}
+
+func (*addInitTables) Name() string {
+	return "Opsgenie init schemas"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/20230123_add_connection_fields.go b/backend/plugins/opsgenie/models/migrationscripts/20230123_add_connection_fields.go
new file mode 100644
index 000000000..ab54d8123
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/20230123_add_connection_fields.go
@@ -0,0 +1,51 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/errors"
+)
+
+type opsgenieConnection20230905 struct {
+	Endpoint         string `mapstructure:"endpoint" env:"OPSGENIE_ENDPOINT" validate:"required"`
+	Proxy            string `mapstructure:"proxy" env:"OPSGENIE_PROXY"`
+	RateLimitPerHour int    `comment:"api request rate limit per hour"`
+}
+
+func (opsgenieConnection20230905) TableName() string {
+	return "_tool_opsgenie_connections"
+}
+
+type addOpsgenieConnectionFields20230905 struct{}
+
+func (script *addOpsgenieConnectionFields20230905) Name() string {
+	return "add connection config fields"
+}
+
+func (script *addOpsgenieConnectionFields20230905) Up(basicRes context.BasicRes) errors.Error {
+	err := basicRes.GetDal().AutoMigrate(&opsgenieConnection20230905{})
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (*addOpsgenieConnectionFields20230905) Version() uint64 {
+	return 20230905000000
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/20230203_add_endpoint_proxy_to_conn.go b/backend/plugins/opsgenie/models/migrationscripts/20230203_add_endpoint_proxy_to_conn.go
new file mode 100644
index 000000000..6a1430e8d
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/20230203_add_endpoint_proxy_to_conn.go
@@ -0,0 +1,50 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+type connection20230203 struct {
+	Endpoint         string `mapstructure:"endpoint" validate:"required" json:"endpoint"`
+	Proxy            string `mapstructure:"proxy" json:"proxy"`
+	RateLimitPerHour int    `comment:"api request rate limit per hour" json:"rateLimit"`
+}
+
+func (*connection20230203) TableName() string {
+	return "_tool_opsgenie_connections"
+}
+
+type addEndpointAndProxyToConnection struct{}
+
+func (*addEndpointAndProxyToConnection) Up(basicRes context.BasicRes) errors.Error {
+	return migrationhelper.AutoMigrateTables(basicRes,
+		&connection20230203{},
+	)
+}
+
+func (*addEndpointAndProxyToConnection) Version() uint64 {
+	return 20230203201814
+}
+
+func (*addEndpointAndProxyToConnection) Name() string {
+	return "add endpoint/proxy to opsgenie connection"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/20230203_add_transformation_rules.go b/backend/plugins/opsgenie/models/migrationscripts/20230203_add_transformation_rules.go
new file mode 100644
index 000000000..0e211e9c5
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/20230203_add_transformation_rules.go
@@ -0,0 +1,50 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models/migrationscripts/archived"
+)
+
+type service20230303 struct {
+	TransformationRuleId uint64
+}
+
+func (*service20230303) TableName() string {
+	return archived.Service{}.TableName()
+}
+
+type addTransformationRulesToService20230303 struct{}
+
+func (*addTransformationRulesToService20230303) Up(basicRes context.BasicRes) errors.Error {
+	return migrationhelper.AutoMigrateTables(basicRes,
+		&archived.TransformationRules{},
+		&service20230303{},
+	)
+}
+
+func (*addTransformationRulesToService20230303) Version() uint64 {
+	return 20230303001814
+}
+
+func (*addTransformationRulesToService20230303) Name() string {
+	return "add transformation rules to opsgenie"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/20230531_scope_config.go b/backend/plugins/opsgenie/models/migrationscripts/20230531_scope_config.go
new file mode 100644
index 000000000..bbd4c0487
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/20230531_scope_config.go
@@ -0,0 +1,56 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+type renameTr2ScopeConfig struct {
+}
+
+type scopeConfig20230531 struct {
+	Entities []string `gorm:"type:json" json:"entities"`
+}
+
+func (scopeConfig20230531) TableName() string {
+	return "_tool_opsgenie_scope_configs"
+}
+
+func (u *renameTr2ScopeConfig) Up(baseRes context.BasicRes) errors.Error {
+	db := baseRes.GetDal()
+	err := db.RenameColumn("_tool_opsgenie_services", "transformation_rule_id", "scope_config_id")
+	if err != nil {
+		return err
+	}
+	err = db.RenameTable("_tool_opsgenie_transformation_rules", "_tool_opsgenie_scope_configs")
+	if err != nil {
+		return err
+	}
+	return migrationhelper.AutoMigrateTables(baseRes, &scopeConfig20230531{})
+}
+
+func (*renameTr2ScopeConfig) Version() uint64 {
+	return 20230531130109
+}
+
+func (*renameTr2ScopeConfig) Name() string {
+	return "rename transformation rule to scope config for opsgenie"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/20230605_remove_scope_config.go b/backend/plugins/opsgenie/models/migrationscripts/20230605_remove_scope_config.go
new file mode 100644
index 000000000..1b3fa4ed9
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/20230605_remove_scope_config.go
@@ -0,0 +1,39 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/errors"
+)
+
+type removeScopeConfig struct {
+}
+
+func (u *removeScopeConfig) Up(baseRes context.BasicRes) errors.Error {
+	db := baseRes.GetDal()
+	return db.DropTables("_tool_opsgenie_scope_configs")
+}
+
+func (*removeScopeConfig) Version() uint64 {
+	return 20230605130109
+}
+
+func (*removeScopeConfig) Name() string {
+	return "remove _tool_opsgenie_scope_configs table"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/20230630_add_raw_param_table_for_scopes.go b/backend/plugins/opsgenie/models/migrationscripts/20230630_add_raw_param_table_for_scopes.go
new file mode 100644
index 000000000..7c5d76a17
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/20230630_add_raw_param_table_for_scopes.go
@@ -0,0 +1,81 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+var _ plugin.MigrationScript = (*addRawParamTableForScope)(nil)
+
+type scope20230630 struct {
+	ConnectionId  uint64 `gorm:"primaryKey"`
+	Id            string `gorm:"primaryKey"`
+	RawDataTable  string `gorm:"column:_raw_data_table"`
+	RawDataParams string `gorm:"column:_raw_data_params"`
+}
+
+func (scope20230630) TableName() string {
+	return "_tool_opsgenie_services"
+}
+
+type params20230630 struct {
+	ConnectionId uint64
+	ScopeId      string
+}
+
+type addRawParamTableForScope struct{}
+
+func (script *addRawParamTableForScope) Up(basicRes context.BasicRes) errors.Error {
+	db := basicRes.GetDal()
+	return migrationhelper.CopyTableColumns(basicRes,
+		scope20230630{}.TableName(),
+		scope20230630{}.TableName(),
+		func(src *scope20230630) (*scope20230630, errors.Error) {
+			src.RawDataTable = "_raw_opsgenie_scopes"
+			src.RawDataParams = string(errors.Must1(json.Marshal(&params20230630{
+				ConnectionId: src.ConnectionId,
+				ScopeId:      src.Id,
+			})))
+			updateSet := []dal.DalSet{
+				{ColumnName: "_raw_data_table", Value: src.RawDataTable},
+				{ColumnName: "_raw_data_params", Value: src.RawDataParams},
+			}
+			where := dal.Where("id = ?", fmt.Sprintf("opsgenie:Service:%v:%v", src.ConnectionId, src.Id))
+			errors.Must(db.UpdateColumns("repos", updateSet, where))
+			errors.Must(db.UpdateColumns("boards", updateSet, where))
+			errors.Must(db.UpdateColumns("cicd_scopes", updateSet, where))
+			errors.Must(db.UpdateColumns("cq_projects", updateSet, where))
+			return src, nil
+		})
+}
+
+func (*addRawParamTableForScope) Version() uint64 {
+	return 20230630000002
+}
+
+func (script *addRawParamTableForScope) Name() string {
+	return "populated _raw_data columns for opsgenie services"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/archived/assignment.go b/backend/plugins/opsgenie/models/migrationscripts/archived/assignment.go
new file mode 100644
index 000000000..e6debcf87
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/archived/assignment.go
@@ -0,0 +1,33 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type Assignment struct {
+	archived.NoPKModel
+	ConnectionId uint64
+	ResponderId  string `gorm:"primaryKey"`
+	IncidentId   string `gorm:"primaryKey"`
+}
+
+func (Assignment) TableName() string {
+	return "_tool_opsgenie_assignments"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/archived/connection.go b/backend/plugins/opsgenie/models/migrationscripts/archived/connection.go
new file mode 100644
index 000000000..d4882c493
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/archived/connection.go
@@ -0,0 +1,32 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type OpsgenieConnection struct {
+	archived.Model
+	Name  string `gorm:"type:varchar(100);uniqueIndex" json:"name" validate:"required"`
+	Token string `mapstructure:"token" env:"OPSGENIE_AUTH" validate:"required" encrypt:"yes"`
+}
+
+func (OpsgenieConnection) TableName() string {
+	return "_tool_opsgenie_connections"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/archived/incident.go b/backend/plugins/opsgenie/models/migrationscripts/archived/incident.go
new file mode 100644
index 000000000..6611093ef
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/archived/incident.go
@@ -0,0 +1,44 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"time"
+
+	"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type Incident struct {
+	archived.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey"`
+	Id           string `gorm:"primaryKey"`
+	Url          string
+	ServiceId    string
+	ServiceName  string
+	Description  string
+	Message      string
+	OwnerTeam    string
+	Priority     string
+	Status       string
+	CreatedDate  time.Time
+	UpdatedDate  time.Time
+}
+
+func (Incident) TableName() string {
+	return "_tool_opsgenie_incidents"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/archived/responder.go b/backend/plugins/opsgenie/models/migrationscripts/archived/responder.go
new file mode 100644
index 000000000..c3507f185
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/archived/responder.go
@@ -0,0 +1,33 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import "github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+
+type Responder struct {
+	archived.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey"`
+	Id           string `gorm:"primaryKey;autoIncremental:false"`
+	Type         string
+	FullName     string
+	Name         string
+}
+
+func (Responder) TableName() string {
+	return "_tool_opsgenie_responders"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/archived/service.go b/backend/plugins/opsgenie/models/migrationscripts/archived/service.go
new file mode 100644
index 000000000..1daddb734
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/archived/service.go
@@ -0,0 +1,37 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type (
+	Service struct {
+		archived.NoPKModel
+		ConnectionId uint64 `gorm:"primaryKey"`
+		Id           string `gorm:"primaryKey;autoIncremental:false"`
+		Url          string
+		Name         string
+		TeamId       string
+	}
+)
+
+func (Service) TableName() string {
+	return "_tool_opsgenie_services"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/archived/team.go b/backend/plugins/opsgenie/models/migrationscripts/archived/team.go
new file mode 100644
index 000000000..d7f8f1b6a
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/archived/team.go
@@ -0,0 +1,32 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import "github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+
+type Team struct {
+	archived.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey"`
+	Id           string `gorm:"primaryKey;autoIncrement:false"`
+	Name         string
+	Description  string
+}
+
+func (Team) TableName() string {
+	return "_tool_opsgenie_teams"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/archived/transformation_rules.go b/backend/plugins/opsgenie/models/migrationscripts/archived/transformation_rules.go
new file mode 100644
index 000000000..35a8dfeb9
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/archived/transformation_rules.go
@@ -0,0 +1,32 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+	"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type TransformationRules struct {
+	archived.Model
+	Name         string `gorm:"type:varchar(255);index:idx_name_github,unique"`
+	ConnectionId uint64
+}
+
+func (*TransformationRules) TableName() string {
+	return "_tool_opsgenie_transformation_rules"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/archived/user.go b/backend/plugins/opsgenie/models/migrationscripts/archived/user.go
new file mode 100644
index 000000000..5b37650b2
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/archived/user.go
@@ -0,0 +1,32 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import "github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+
+type User struct {
+	archived.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey"`
+	Id           string `gorm:"primaryKey;autoIncrement:false"`
+	Username     string
+	FullName     string
+}
+
+func (User) TableName() string {
+	return "_tool_opsgenie_users"
+}
diff --git a/backend/plugins/opsgenie/models/migrationscripts/register.go b/backend/plugins/opsgenie/models/migrationscripts/register.go
new file mode 100644
index 000000000..12733bb04
--- /dev/null
+++ b/backend/plugins/opsgenie/models/migrationscripts/register.go
@@ -0,0 +1,35 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/plugin"
+)
+
+// All return all the migration scripts
+func All() []plugin.MigrationScript {
+	return []plugin.MigrationScript{
+		new(addInitTables),
+		new(addOpsgenieConnectionFields20230905),
+		new(addEndpointAndProxyToConnection),
+		new(addTransformationRulesToService20230303),
+		new(addRawParamTableForScope),
+		new(renameTr2ScopeConfig),
+		new(removeScopeConfig),
+	}
+}
diff --git a/backend/plugins/opsgenie/models/raw/incident.go b/backend/plugins/opsgenie/models/raw/incident.go
new file mode 100644
index 000000000..3b8a2692e
--- /dev/null
+++ b/backend/plugins/opsgenie/models/raw/incident.go
@@ -0,0 +1,45 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package raw
+
+import "time"
+
+type Incident struct {
+	Id               *string    `json:"id"`
+	Description      *string    `json:"description"`
+	ImpactedServices *[]string  `json:"impactedServices"`
+	TinyId           *string    `json:"tinyId"`
+	Message          *string    `json:"message"`
+	Status           *string    `json:"status"`
+	Tags             []any      `json:"tags"`
+	CreatedAt        *time.Time `json:"createdAt"`
+	UpdatedAt        *time.Time `json:"updatedAt"`
+	Priority         *string    `json:"priority"`
+	OwnerTeam        *string    `json:"ownerTeam"`
+	Responders       *[]struct {
+		Type *string `json:"type"`
+		Id   *string `json:"id"`
+	} `json:"responders"`
+	ExtraProperties struct {
+	} `json:"extraProperties"`
+	Links struct {
+		Web *string `json:"web"`
+		Api *string `json:"api"`
+	} `json:"links"`
+	Actions *[]any `json:"actions"`
+}
diff --git a/backend/plugins/opsgenie/models/raw/service.go b/backend/plugins/opsgenie/models/raw/service.go
new file mode 100644
index 000000000..ead3dd4e4
--- /dev/null
+++ b/backend/plugins/opsgenie/models/raw/service.go
@@ -0,0 +1,31 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package raw
+
+type Service struct {
+	Id          string `json:"id"`
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	TeamId      string `json:"teamId"`
+	Tags        []any  `json:"tags"`
+	Links       struct {
+		Web string `json:"web"`
+		Api string `json:"api"`
+	} `json:"links"`
+	IsExternal bool `json:"isExternal"`
+}
diff --git a/backend/plugins/opsgenie/models/raw/team.go b/backend/plugins/opsgenie/models/raw/team.go
new file mode 100644
index 000000000..a1086ef77
--- /dev/null
+++ b/backend/plugins/opsgenie/models/raw/team.go
@@ -0,0 +1,28 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package raw
+
+type Team struct {
+	Id          *string `json:"id"`
+	Name        *string `json:"name"`
+	Description *string `json:"description"`
+	Links       *struct {
+		Web *string `json:"web"`
+		API *string `json:"api"`
+	} `json:"links"`
+}
diff --git a/backend/plugins/opsgenie/models/raw/user.go b/backend/plugins/opsgenie/models/raw/user.go
new file mode 100644
index 000000000..4be271363
--- /dev/null
+++ b/backend/plugins/opsgenie/models/raw/user.go
@@ -0,0 +1,42 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package raw
+
+import "time"
+
+type User struct {
+	Blocked  *bool   `json:"blocked"`
+	Verified *bool   `json:"verified"`
+	Id       *string `json:"id"`
+	Username *string `json:"username"`
+	FullName *string `json:"fullName"`
+	Role     *struct {
+		Id   *string `json:"id"`
+		Name *string `json:"name"`
+	} `json:"role"`
+	TimeZone    *string `json:"timeZone"`
+	Locale      *string `json:"locale"`
+	UserAddress *struct {
+		Country *string `json:"country"`
+		State   *string `json:"state"`
+		City    *string `json:"city"`
+		Line    *string `json:"line"`
+		ZipCode *string `json:"zipCode"`
+	} `json:"userAddress"`
+	CreatedAt *time.Time `json:"createdAt"`
+}
diff --git a/backend/plugins/opsgenie/models/responder.go b/backend/plugins/opsgenie/models/responder.go
new file mode 100644
index 000000000..62b776ca9
--- /dev/null
+++ b/backend/plugins/opsgenie/models/responder.go
@@ -0,0 +1,35 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/core/models/common"
+)
+
+type Responder struct {
+	common.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey"`
+	Id           string `gorm:"primaryKey;autoIncrement:false"`
+	Type         string
+	FullName     string
+	Name         string
+}
+
+func (Responder) TableName() string {
+	return "_tool_opsgenie_responders"
+}
diff --git a/backend/plugins/opsgenie/models/scope_config.go b/backend/plugins/opsgenie/models/scope_config.go
new file mode 100644
index 000000000..526dc6ef7
--- /dev/null
+++ b/backend/plugins/opsgenie/models/scope_config.go
@@ -0,0 +1,24 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import "github.com/apache/incubator-devlake/core/models/common"
+
+type OpsenieScopeConfig struct {
+	common.ScopeConfig `mapstructure:",squash" json:",inline" gorm:"embedded"`
+}
diff --git a/backend/plugins/opsgenie/models/service.go b/backend/plugins/opsgenie/models/service.go
new file mode 100644
index 000000000..71fd68420
--- /dev/null
+++ b/backend/plugins/opsgenie/models/service.go
@@ -0,0 +1,69 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/core/models/common"
+	"github.com/apache/incubator-devlake/core/plugin"
+)
+
+type OpsgenieParams struct {
+	ConnectionId uint64
+	ScopeId      string
+}
+
+type Service struct {
+	common.Scope `mapstructure:",squash"`
+	Id           string `json:"id" mapstructure:"id" gorm:"primaryKey;autoIncrement:false" `
+	Url          string `json:"url" mapstructure:"url"`
+	Name         string `json:"name" mapstructure:"name"`
+	TeamId       string `json:"team_id" mapstructure:"team_id"`
+}
+
+func (s Service) ScopeId() string {
+	return s.Name
+}
+
+// func (s Service) ScopeConnectionId() uint64 {
+// 	return s.ConnectionId
+// }
+
+// func (s Service) ScopeScopeConfigId() uint64 {
+// 	return 0
+// }
+
+func (s Service) ScopeName() string {
+	return s.Name
+}
+
+func (s Service) ScopeFullName() string {
+	return s.Name
+}
+
+func (s Service) ScopeParams() interface{} {
+	return &OpsgenieParams{
+		ConnectionId: s.ConnectionId,
+		ScopeId:      s.Id,
+	}
+}
+
+func (s Service) TableName() string {
+	return "_tool_opsgenie_services"
+}
+
+var _ plugin.ToolLayerScope = (*Service)(nil)
diff --git a/backend/plugins/opsgenie/models/team.go b/backend/plugins/opsgenie/models/team.go
new file mode 100644
index 000000000..90053d848
--- /dev/null
+++ b/backend/plugins/opsgenie/models/team.go
@@ -0,0 +1,34 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/core/models/common"
+)
+
+type Team struct {
+	common.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey"`
+	Id           string `gorm:"primaryKey;autoIncrement:false"`
+	Name         string
+	Description  string
+}
+
+func (Team) TableName() string {
+	return "_tool_opsgenie_teams"
+}
diff --git a/backend/plugins/opsgenie/models/user.go b/backend/plugins/opsgenie/models/user.go
new file mode 100644
index 000000000..39772529c
--- /dev/null
+++ b/backend/plugins/opsgenie/models/user.go
@@ -0,0 +1,34 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+	"github.com/apache/incubator-devlake/core/models/common"
+)
+
+type User struct {
+	common.NoPKModel
+	ConnectionId uint64 `gorm:"primaryKey"`
+	Id           string `gorm:"primaryKey;autoIncrement:false"`
+	Username     string
+	FullName     string
+}
+
+func (User) TableName() string {
+	return "_tool_opsgenie_users"
+}
diff --git a/backend/plugins/opsgenie/opsgenie.go b/backend/plugins/opsgenie/opsgenie.go
new file mode 100644
index 000000000..d9402a02f
--- /dev/null
+++ b/backend/plugins/opsgenie/opsgenie.go
@@ -0,0 +1,38 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"github.com/apache/incubator-devlake/core/runner"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/impl"
+	"github.com/spf13/cobra"
+)
+
+// PluginEntry is a variable exported for Framework to search and load
+var PluginEntry impl.Opsgenie //nolint
+
+// standalone mode for debugging
+func main() {
+	cmd := &cobra.Command{Use: "opsgenie"}
+	timeAfter := cmd.Flags().StringP("timeAfter", "a", "", "collect data that are created after specified time, ie 2006-01-02T15:04:05Z")
+
+	cmd.Run = func(cmd *cobra.Command, args []string) {
+		runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{}, *timeAfter)
+	}
+	runner.RunCmd(cmd)
+}
diff --git a/backend/plugins/opsgenie/tasks/incidents_collector.go b/backend/plugins/opsgenie/tasks/incidents_collector.go
new file mode 100644
index 000000000..6d9db5c18
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/incidents_collector.go
@@ -0,0 +1,136 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"reflect"
+	"time"
+
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+)
+
+const RAW_INCIDENTS_TABLE = "opsgenie_incidents"
+
+var _ plugin.SubTaskEntryPoint = CollectIncidents
+
+type (
+	collectedIncidents struct {
+		TotalCount int               `json:"totalCount"`
+		Data       []json.RawMessage `json:"data"`
+	}
+	collectedIncident struct {
+		Data json.RawMessage `json:"data"`
+	}
+	simplifiedRawIncident struct {
+		Id        string    `json:"id"`
+		CreatedAt time.Time `json:"createdAt"`
+	}
+)
+
+var CollectIncidentsMeta = plugin.SubTaskMeta{
+	Name:             "collectIncidents",
+	EntryPoint:       CollectIncidents,
+	EnabledByDefault: true,
+	Description:      "Collect Opsgenie incidents",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_TICKET},
+	DependencyTables: []string{},
+	ProductTables:    []string{RAW_INCIDENTS_TABLE},
+}
+
+func CollectIncidents(taskCtx plugin.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	db := taskCtx.GetDal()
+	args := api.RawDataSubTaskArgs{
+		Ctx:     taskCtx,
+		Options: data.Options,
+		Table:   RAW_INCIDENTS_TABLE,
+	}
+	collector, err := api.NewStatefulApiCollectorForFinalizableEntity(api.FinalizableApiCollectorArgs{
+		RawDataSubTaskArgs: args,
+		ApiClient:          data.Client,
+		CollectNewRecordsByList: api.FinalizableApiCollectorListArgs{
+			PageSize:    100,
+			Concurrency: 10,
+			FinalizableApiCollectorCommonArgs: api.FinalizableApiCollectorCommonArgs{
+				UrlTemplate: "v1/incidents",
+				Query: func(reqData *api.RequestData, createdAfter *time.Time) (url.Values, errors.Error) {
+					query := url.Values{}
+					incidentQueryParams := fmt.Sprintf("impactedServices:%s", data.Options.ServiceId)
+
+					query.Set("query", incidentQueryParams)
+					query.Set("sort", "createdAt")
+					query.Set("order", "desc")
+					query.Set("limit", fmt.Sprintf("%d", reqData.Pager.Size))
+					query.Set("offset", fmt.Sprintf("%d", reqData.Pager.Skip))
+					return query, nil
+				},
+				ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+					rawResult := collectedIncidents{}
+					err := api.UnmarshalResponse(res, &rawResult)
+
+					return rawResult.Data, err
+				},
+			},
+			GetCreated: func(item json.RawMessage) (time.Time, errors.Error) {
+				i := &simplifiedRawIncident{}
+				err := json.Unmarshal(item, i)
+				if err != nil {
+					return time.Time{}, errors.BadInput.Wrap(err, "failed to unmarshal opsgenie incidents")
+				}
+				return i.CreatedAt, nil
+			},
+		},
+		CollectUnfinishedDetails: &api.FinalizableApiCollectorDetailArgs{
+			BuildInputIterator: func() (api.Iterator, errors.Error) {
+				cursor, err := db.Cursor(
+					dal.Select("id"),
+					dal.From(&models.Incident{}),
+					dal.Where(
+						"service_id = ? AND connection_id = ? AND status NOT IN ('resolved', 'closed')",
+						data.Options.ServiceId, data.Options.ConnectionId,
+					),
+				)
+				if err != nil {
+					return nil, err
+				}
+				return api.NewDalCursorIterator(db, cursor, reflect.TypeOf(simplifiedRawIncident{}))
+			},
+			FinalizableApiCollectorCommonArgs: api.FinalizableApiCollectorCommonArgs{
+				UrlTemplate: "v1/incidents/{{ .Input.Id }}",
+				ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+					rawResult := collectedIncident{}
+					err := api.UnmarshalResponse(res, &rawResult)
+
+					return []json.RawMessage{rawResult.Data}, err
+				},
+			},
+		},
+	})
+	if err != nil {
+		return nil
+	}
+	return collector.Execute()
+}
diff --git a/backend/plugins/opsgenie/tasks/incidents_converter.go b/backend/plugins/opsgenie/tasks/incidents_converter.go
new file mode 100644
index 000000000..82d5406d2
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/incidents_converter.go
@@ -0,0 +1,168 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"reflect"
+	"time"
+
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/models/common"
+	"github.com/apache/incubator-devlake/core/models/domainlayer"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+)
+
+var ConvertIncidentsMeta = plugin.SubTaskMeta{
+	Name:             "convertIncidents",
+	EntryPoint:       ConvertIncidents,
+	EnabledByDefault: true,
+	Description:      "Convert Incidents into domain layer table issues",
+	Dependencies: []*plugin.SubTaskMeta{
+		&ExtractUsersMeta,
+		&ExtractTeamsMeta,
+		&CollectUsersMeta,
+		&CollectTeamsMeta,
+		&ConvertUsersMeta,
+		&ConvertTeamsMeta,
+	},
+	DependencyTables: []string{
+		models.User{}.TableName(), // cursor
+		models.Team{}.TableName(), // cursor
+	},
+	DomainTypes: []string{plugin.DOMAIN_TYPE_TICKET},
+}
+
+type (
+	IncidentWithResponder struct {
+		common.NoPKModel
+		models.Incident
+		*models.Responder
+	}
+)
+
+func ConvertIncidents(taskCtx plugin.SubTaskContext) errors.Error {
+	db := taskCtx.GetDal()
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+
+	// Correlates responder id with it's user or team name
+	cursor, err := db.Cursor(
+		dal.Select("incidents.*, responders.id, responders.type, users.full_name, teams.name"),
+		dal.From("_tool_opsgenie_incidents AS incidents"),
+		dal.Join(`LEFT JOIN _tool_opsgenie_assignments AS assignments ON assignments.incident_id = incidents.id`),
+		dal.Join(`LEFT JOIN _tool_opsgenie_responders AS responders ON assignments.responder_id = responders.id`),
+		dal.Join(`LEFT JOIN _tool_opsgenie_users AS users ON users.id = responders.id`),
+		dal.Join(`LEFT JOIN _tool_opsgenie_teams AS teams ON teams.id = responders.id`),
+		dal.Where("incidents.connection_id = ? AND incidents.service_id = ?", data.Options.ConnectionId, data.Options.ServiceId),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	defer cursor.Close()
+
+	idGen := didgen.NewDomainIdGenerator(&models.Incident{})
+	serviceIdGen := didgen.NewDomainIdGenerator(&models.Service{})
+	converter, err := api.NewDataConverter(api.DataConverterArgs{
+		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+			Ctx:     taskCtx,
+			Options: data.Options,
+			Table:   RAW_INCIDENTS_TABLE,
+		},
+		InputRowType: reflect.TypeOf(IncidentWithResponder{}),
+		Input:        cursor,
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			combined := inputRow.(*IncidentWithResponder)
+			incident := combined.Incident
+			status := getStatus(&incident)
+			leadTime, resolutionDate := getTimes(&incident)
+			domainIssue := &ticket.Issue{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: idGen.Generate(data.Options.ConnectionId, incident.Id),
+				},
+				Url:             incident.Url,
+				IssueKey:        incident.Id,
+				Title:           incident.Message,
+				Description:     incident.Description,
+				Type:            ticket.INCIDENT,
+				Status:          status,
+				OriginalStatus:  string(incident.Status),
+				ResolutionDate:  resolutionDate,
+				CreatedDate:     &incident.CreatedDate,
+				UpdatedDate:     &incident.UpdatedDate,
+				LeadTimeMinutes: leadTime,
+				Priority:        string(incident.Priority),
+			}
+			var result []interface{}
+			if combined.Responder != nil {
+				var assigneeName string
+				if combined.Responder.Type == "user" {
+					assigneeName = combined.Responder.FullName
+				}
+				if combined.Responder.Type == "team" {
+					assigneeName = combined.Responder.Name
+				}
+				issueAssignee := &ticket.IssueAssignee{
+					IssueId:      domainIssue.Id,
+					AssigneeId:   combined.Responder.Id,
+					AssigneeName: resolve(&assigneeName),
+				}
+				result = append(result, issueAssignee)
+			}
+			result = append(result, domainIssue)
+			boardIssue := &ticket.BoardIssue{
+				BoardId: serviceIdGen.Generate(data.Options.ConnectionId, data.Options.ServiceId),
+				IssueId: domainIssue.Id,
+			}
+			result = append(result, boardIssue)
+			return result, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+	return converter.Execute()
+}
+
+func getStatus(incident *models.Incident) string {
+	if incident.Status == models.IncidentStatusClosed {
+		return ticket.OTHER
+	}
+	if incident.Status == models.IncidentStatusOpen {
+		return ticket.IN_PROGRESS
+	}
+	if incident.Status == models.IncidentStatusResolved {
+		return ticket.DONE
+	}
+	panic("unknown incident status encountered")
+}
+
+func getTimes(incident *models.Incident) (int64, *time.Time) {
+	var leadTime int64
+	var resolutionDate *time.Time
+	if incident.Status == models.IncidentStatusResolved {
+		resolutionDate = &incident.UpdatedDate
+		leadTime = int64(resolutionDate.Sub(incident.CreatedDate).Minutes())
+	}
+	return leadTime, resolutionDate
+}
diff --git a/backend/plugins/opsgenie/tasks/incidents_extractor.go b/backend/plugins/opsgenie/tasks/incidents_extractor.go
new file mode 100644
index 000000000..1f0066f26
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/incidents_extractor.go
@@ -0,0 +1,95 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models/raw"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractIncidents
+
+var ExtractIncidentsMeta = plugin.SubTaskMeta{
+	Name:             "extractIncidents",
+	EntryPoint:       ExtractIncidents,
+	EnabledByDefault: true,
+	Description:      "Extract Opsgenie incidents",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_TICKET},
+	DependencyTables: []string{RAW_INCIDENTS_TABLE},
+	ProductTables:    []string{models.Incident{}.TableName()},
+}
+
+func ExtractIncidents(taskCtx plugin.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+			Ctx:     taskCtx,
+			Options: data.Options,
+			Table:   RAW_INCIDENTS_TABLE,
+		},
+		Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+			incidentRaw := &raw.Incident{}
+			err := errors.Convert(json.Unmarshal(row.Data, incidentRaw))
+			if err != nil {
+				return nil, err
+			}
+
+			results := make([]interface{}, 0, 1)
+			incident := models.Incident{
+				ConnectionId: data.Options.ConnectionId,
+				Id:           *incidentRaw.Id,
+				Url:          resolve(incidentRaw.Links.Web),
+				Message:      *incidentRaw.Message,
+				OwnerTeam:    resolve(incidentRaw.OwnerTeam),
+				Description:  resolve(incidentRaw.Description),
+				ServiceId:    data.Options.ServiceId,
+				ServiceName:  data.Options.ServiceName,
+				Status:       models.IncidentStatus(*incidentRaw.Status),
+				Priority:     models.IncidentPriority(*incidentRaw.Priority),
+				CreatedDate:  *incidentRaw.CreatedAt,
+				UpdatedDate:  *incidentRaw.UpdatedAt,
+			}
+			results = append(results, &incident)
+			for _, responderRaw := range *incidentRaw.Responders {
+				if err != nil {
+					return nil, err
+				}
+				results = append(results, &models.Assignment{
+					ConnectionId: data.Options.ConnectionId,
+					ResponderId:  *responderRaw.Id,
+					IncidentId:   *incidentRaw.Id,
+				})
+				results = append(results, &models.Responder{
+					ConnectionId: data.Options.ConnectionId,
+					Id:           *responderRaw.Id,
+					Type:         *responderRaw.Type,
+				})
+			}
+			return results, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+	return extractor.Execute()
+}
diff --git a/backend/plugins/opsgenie/tasks/service_converter.go b/backend/plugins/opsgenie/tasks/service_converter.go
new file mode 100644
index 000000000..d8a468384
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/service_converter.go
@@ -0,0 +1,83 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"reflect"
+
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/models/domainlayer"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/core/plugin"
+	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+)
+
+var ConvertServicesMeta = plugin.SubTaskMeta{
+	Name:             "convertServices",
+	EntryPoint:       ConvertServices,
+	EnabledByDefault: true,
+	Description:      "Convert Opsgenie services",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_TICKET},
+}
+
+func ConvertServices(taskCtx plugin.SubTaskContext) errors.Error {
+	db := taskCtx.GetDal()
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	rawDataSubTaskArgs := &helper.RawDataSubTaskArgs{
+		Ctx:     taskCtx,
+		Options: data.Options,
+		Table:   "opsgenie_services",
+	}
+	clauses := []dal.Clause{
+		dal.Select("services.*"),
+		dal.From("_tool_opsgenie_services services"),
+		dal.Where("services.id = ? and services.connection_id = ?", data.Options.ServiceId, data.Options.ConnectionId),
+	}
+
+	cursor, err := db.Cursor(clauses...)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+
+	converter, err := helper.NewDataConverter(helper.DataConverterArgs{
+		RawDataSubTaskArgs: *rawDataSubTaskArgs,
+		InputRowType:       reflect.TypeOf(models.Service{}),
+		Input:              cursor,
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			service := inputRow.(*models.Service)
+			domainBoard := &ticket.Board{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: didgen.NewDomainIdGenerator(service).Generate(service.ConnectionId, service.Id),
+				},
+				Name: service.Name,
+				Url:  service.Url,
+			}
+			return []interface{}{
+				domainBoard,
+			}, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+	return converter.Execute()
+}
diff --git a/backend/plugins/opsgenie/tasks/shared.go b/backend/plugins/opsgenie/tasks/shared.go
new file mode 100644
index 000000000..795903ec2
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/shared.go
@@ -0,0 +1,25 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+func resolve[T any](t *T) T {
+	if t == nil {
+		return *new(T)
+	}
+	return *t
+}
diff --git a/backend/plugins/opsgenie/tasks/task_data.go b/backend/plugins/opsgenie/tasks/task_data.go
new file mode 100644
index 000000000..5d20fc820
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/task_data.go
@@ -0,0 +1,88 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+)
+
+type OpsgenieOptions struct {
+	ConnectionId uint64   `json:"connectionId"`
+	ServiceId    string   `json:"service_id,omitempty"`
+	ServiceName  string   `json:"service_name,omitempty"`
+	Tasks        []string `json:"tasks,omitempty"`
+	*models.OpsenieScopeConfig
+}
+
+type OpsgenieTaskData struct {
+	Options *OpsgenieOptions
+	Client  api.RateLimitedApiClient
+}
+
+func (p *OpsgenieOptions) GetParams() any {
+	return models.OpsgenieParams{
+		ConnectionId: p.ConnectionId,
+		ScopeId:      p.ServiceId,
+	}
+}
+
+func DecodeAndValidateTaskOptions(options map[string]interface{}) (*OpsgenieOptions, errors.Error) {
+	op, err := DecodeTaskOptions(options)
+	if err != nil {
+		return nil, err
+	}
+	err = ValidateTaskOptions(op)
+	if err != nil {
+		return nil, err
+	}
+	return op, nil
+}
+
+func DecodeTaskOptions(options map[string]interface{}) (*OpsgenieOptions, errors.Error) {
+	var op OpsgenieOptions
+	err := api.Decode(options, &op, nil)
+	if err != nil {
+		return nil, err
+	}
+	return &op, nil
+}
+
+func EncodeTaskOptions(op *OpsgenieOptions) (map[string]interface{}, errors.Error) {
+	var result map[string]interface{}
+	err := api.Decode(op, &result, nil)
+	if err != nil {
+		return nil, err
+	}
+	return result, nil
+}
+
+func ValidateTaskOptions(op *OpsgenieOptions) errors.Error {
+	if op.ServiceName == "" {
+		return errors.BadInput.New("not enough info for Opsgenie execution")
+	}
+	if op.ServiceId == "" {
+		return errors.BadInput.New("not enough info for Opsgenie execution")
+	}
+	// find the needed GitHub now
+	if op.ConnectionId == 0 {
+		return errors.BadInput.New("connectionId is invalid")
+	}
+	return nil
+}
diff --git a/backend/plugins/opsgenie/tasks/teams_collector.go b/backend/plugins/opsgenie/tasks/teams_collector.go
new file mode 100644
index 000000000..400ebece2
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/teams_collector.go
@@ -0,0 +1,69 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"net/http"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+const RAW_TEAMS_TABLE = "opsgenie_teams"
+
+type (
+	collectedTeams struct {
+		TotalCount int               `json:"totalCount"`
+		Data       []json.RawMessage `json:"data"`
+	}
+)
+
+var CollectTeamsMeta = plugin.SubTaskMeta{
+	Name:             "collectTeams",
+	EntryPoint:       CollectTeams,
+	EnabledByDefault: true,
+	Description:      "collect Opsgenie teams.",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_CROSS},
+}
+
+func CollectTeams(taskCtx plugin.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	collector, err := api.NewApiCollector(api.ApiCollectorArgs{
+		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+			Ctx:     taskCtx,
+			Options: data.Options,
+			Table:   RAW_TEAMS_TABLE,
+		},
+		ApiClient:   data.Client,
+		UrlTemplate: "v2/teams",
+		Query:       nil,
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+			rawResult := collectedTeams{}
+			err := api.UnmarshalResponse(res, &rawResult)
+
+			return rawResult.Data, err
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return collector.Execute()
+}
diff --git a/backend/plugins/opsgenie/tasks/teams_convertor.go b/backend/plugins/opsgenie/tasks/teams_convertor.go
new file mode 100644
index 000000000..08a1e58fe
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/teams_convertor.go
@@ -0,0 +1,82 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"reflect"
+
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/models/domainlayer"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/crossdomain"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+)
+
+var ConvertTeamsMeta = plugin.SubTaskMeta{
+	Name:             "convertTeams",
+	EntryPoint:       ConvertTeams,
+	EnabledByDefault: true,
+	Description:      "convert Jira teams",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_CROSS},
+}
+
+func ConvertTeams(taskCtx plugin.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	connectionId := data.Options.ConnectionId
+	db := taskCtx.GetDal()
+	clauses := []dal.Clause{
+		dal.Select("teams.*"),
+		dal.From("_tool_opsgenie_teams teams"),
+		dal.Where("teams.connection_id = ?", connectionId),
+	}
+	cursor, err := db.Cursor(clauses...)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+
+	accountIdGen := didgen.NewDomainIdGenerator(&models.Team{})
+	converter, err := api.NewDataConverter(api.DataConverterArgs{
+		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+			Ctx:     taskCtx,
+			Options: data.Options,
+			Table:   RAW_TEAMS_TABLE,
+		},
+		InputRowType: reflect.TypeOf(models.Team{}),
+		Input:        cursor,
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			team := inputRow.(*models.Team)
+			u := &crossdomain.Team{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: accountIdGen.Generate(connectionId, team.Id),
+				},
+				Name:  team.Id,
+				Alias: team.Name,
+			}
+			return []interface{}{u}, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return converter.Execute()
+}
diff --git a/backend/plugins/opsgenie/tasks/teams_extractor.go b/backend/plugins/opsgenie/tasks/teams_extractor.go
new file mode 100644
index 000000000..0ed6357b6
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/teams_extractor.go
@@ -0,0 +1,70 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models/raw"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractTeams
+
+var ExtractTeamsMeta = plugin.SubTaskMeta{
+	Name:             "extractTeams",
+	EntryPoint:       ExtractTeams,
+	EnabledByDefault: true,
+	Description:      "extract Opsgenie teams",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_CROSS},
+}
+
+func ExtractTeams(taskCtx plugin.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+			Ctx:     taskCtx,
+			Options: data.Options,
+			Table:   RAW_TEAMS_TABLE,
+		},
+		Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+			teamRaw := &raw.Team{}
+			err := errors.Convert(json.Unmarshal(row.Data, teamRaw))
+			if err != nil {
+				return nil, err
+			}
+
+			results := make([]interface{}, 0, 1)
+			user := models.Team{
+				ConnectionId: data.Options.ConnectionId,
+				Id:           *teamRaw.Id,
+				Name:         *teamRaw.Name,
+				Description:  *teamRaw.Description,
+			}
+			results = append(results, &user)
+			return results, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+	return extractor.Execute()
+}
diff --git a/backend/plugins/opsgenie/tasks/users_collector.go b/backend/plugins/opsgenie/tasks/users_collector.go
new file mode 100644
index 000000000..d2b33028d
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/users_collector.go
@@ -0,0 +1,80 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+const RAW_USERS_TABLE = "opsgenie_users"
+
+type (
+	collectedUsers struct {
+		TotalCount int               `json:"totalCount"`
+		Data       []json.RawMessage `json:"data"`
+	}
+)
+
+var CollectUsersMeta = plugin.SubTaskMeta{
+	Name:             "collectUsers",
+	EntryPoint:       CollectUsers,
+	EnabledByDefault: true,
+	Description:      "collect Opsgenie users.",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_CROSS},
+}
+
+func CollectUsers(taskCtx plugin.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	collector, err := api.NewApiCollector(api.ApiCollectorArgs{
+		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+			Ctx:     taskCtx,
+			Options: data.Options,
+			Table:   RAW_USERS_TABLE,
+		},
+		ApiClient:   data.Client,
+		UrlTemplate: "v2/users",
+		PageSize:    100,
+		Query: func(reqData *api.RequestData) (url.Values, errors.Error) {
+			query := url.Values{}
+
+			query.Set("sort", "createdAt")
+			query.Set("order", "desc")
+			query.Set("limit", fmt.Sprintf("%d", reqData.Pager.Size))
+			query.Set("offset", fmt.Sprintf("%d", reqData.Pager.Skip))
+			return query, nil
+		},
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+			rawResult := collectedUsers{}
+			err := api.UnmarshalResponse(res, &rawResult)
+
+			return rawResult.Data, err
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return collector.Execute()
+}
diff --git a/backend/plugins/opsgenie/tasks/users_convertor.go b/backend/plugins/opsgenie/tasks/users_convertor.go
new file mode 100644
index 000000000..ddd9c0095
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/users_convertor.go
@@ -0,0 +1,82 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"reflect"
+
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/models/domainlayer"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/crossdomain"
+	"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+)
+
+var ConvertUsersMeta = plugin.SubTaskMeta{
+	Name:             "convertUsers",
+	EntryPoint:       ConvertUsers,
+	EnabledByDefault: true,
+	Description:      "convert Jira users",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_CROSS},
+}
+
+func ConvertUsers(taskCtx plugin.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	connectionId := data.Options.ConnectionId
+	db := taskCtx.GetDal()
+	clauses := []dal.Clause{
+		dal.Select("users.*"),
+		dal.From("_tool_opsgenie_users users"),
+		dal.Where("users.connection_id = ?", connectionId),
+	}
+	cursor, err := db.Cursor(clauses...)
+	if err != nil {
+		return err
+	}
+	defer cursor.Close()
+
+	accountIdGen := didgen.NewDomainIdGenerator(&models.User{})
+	converter, err := api.NewDataConverter(api.DataConverterArgs{
+		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+			Ctx:     taskCtx,
+			Options: data.Options,
+			Table:   RAW_USERS_TABLE,
+		},
+		InputRowType: reflect.TypeOf(models.User{}),
+		Input:        cursor,
+		Convert: func(inputRow interface{}) ([]interface{}, errors.Error) {
+			user := inputRow.(*models.User)
+			u := &crossdomain.User{
+				DomainEntity: domainlayer.DomainEntity{
+					Id: accountIdGen.Generate(connectionId, user.Id),
+				},
+				Name:  user.FullName,
+				Email: user.Username, // Opsgenie User "username" is user's email
+			}
+			return []interface{}{u}, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return converter.Execute()
+}
diff --git a/backend/plugins/opsgenie/tasks/users_extractor.go b/backend/plugins/opsgenie/tasks/users_extractor.go
new file mode 100644
index 000000000..4482a0f6a
--- /dev/null
+++ b/backend/plugins/opsgenie/tasks/users_extractor.go
@@ -0,0 +1,70 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+	"encoding/json"
+
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/core/plugin"
+	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models"
+	"github.com/apache/incubator-devlake/plugins/opsgenie/models/raw"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractUsers
+
+var ExtractUsersMeta = plugin.SubTaskMeta{
+	Name:             "extractUsers",
+	EntryPoint:       ExtractUsers,
+	EnabledByDefault: true,
+	Description:      "extract Opsgenie users",
+	DomainTypes:      []string{plugin.DOMAIN_TYPE_CROSS},
+}
+
+func ExtractUsers(taskCtx plugin.SubTaskContext) errors.Error {
+	data := taskCtx.GetData().(*OpsgenieTaskData)
+	extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
+		RawDataSubTaskArgs: api.RawDataSubTaskArgs{
+			Ctx:     taskCtx,
+			Options: data.Options,
+			Table:   RAW_USERS_TABLE,
+		},
+		Extract: func(row *api.RawData) ([]interface{}, errors.Error) {
+			userRaw := &raw.User{}
+			err := errors.Convert(json.Unmarshal(row.Data, userRaw))
+			if err != nil {
+				return nil, err
+			}
+
+			results := make([]interface{}, 0, 1)
+			user := models.User{
+				ConnectionId: data.Options.ConnectionId,
+				Id:           *userRaw.Id,
+				Username:     *userRaw.Username,
+				FullName:     *userRaw.FullName,
+			}
+			results = append(results, &user)
+			return results, nil
+		},
+	})
+	if err != nil {
+		return err
+	}
+	return extractor.Execute()
+}
diff --git a/backend/plugins/table_info_test.go b/backend/plugins/table_info_test.go
index 698b215f8..971311933 100644
--- a/backend/plugins/table_info_test.go
+++ b/backend/plugins/table_info_test.go
@@ -37,6 +37,7 @@ import (
 	icla "github.com/apache/incubator-devlake/plugins/icla/impl"
 	jenkins "github.com/apache/incubator-devlake/plugins/jenkins/impl"
 	jira "github.com/apache/incubator-devlake/plugins/jira/impl"
+	opsgenie "github.com/apache/incubator-devlake/plugins/opsgenie/impl"
 	org "github.com/apache/incubator-devlake/plugins/org/impl"
 	pagerduty "github.com/apache/incubator-devlake/plugins/pagerduty/impl"
 	refdiff "github.com/apache/incubator-devlake/plugins/refdiff/impl"
@@ -82,6 +83,7 @@ func Test_GetPluginTablesInfo(t *testing.T) {
 	checker.FeedIn("webhook/models", webhook.Webhook{}.GetTablesInfo)
 	checker.FeedIn("zentao/models", zentao.Zentao{}.GetTablesInfo)
 	checker.FeedIn("circleci/models", circleci.Circleci{}.GetTablesInfo)
+	checker.FeedIn("opsgenie/models", opsgenie.Opsgenie{}.GetTablesInfo)
 	err := checker.Verify()
 	if err != nil {
 		t.Error(err)
diff --git a/config-ui/src/plugins/register/index.ts b/config-ui/src/plugins/register/index.ts
index ecbea7359..f680869ef 100644
--- a/config-ui/src/plugins/register/index.ts
+++ b/config-ui/src/plugins/register/index.ts
@@ -30,6 +30,7 @@ import { SonarQubeConfig } from './sonarqube';
 import { TAPDConfig } from './tapd';
 import { WebhookConfig } from './webhook';
 import { ZenTaoConfig } from './zentao';
+import { OpsgenieConfig } from './opsgenie';
 
 export const pluginConfigs: IPluginConfig[] = [
   AzureConfig,
@@ -44,4 +45,5 @@ export const pluginConfigs: IPluginConfig[] = [
   TAPDConfig,
   ZenTaoConfig,
   WebhookConfig,
+  OpsgenieConfig,
 ].sort((a, b) => a.sort - b.sort);
diff --git a/config-ui/src/plugins/register/opsgenie/assets/icon.svg b/config-ui/src/plugins/register/opsgenie/assets/icon.svg
new file mode 100644
index 000000000..e721717b8
--- /dev/null
+++ b/config-ui/src/plugins/register/opsgenie/assets/icon.svg
@@ -0,0 +1,28 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<svg width="50" height="60" viewBox="0 0 50 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path
+        d="M25.0007 29.896C33.0243 29.896 39.5287 23.2665 39.5287 15.0886C39.5287 6.91074 33.0243 0.28125 25.0007 0.28125C16.9771 0.28125 10.4727 6.91074 10.4727 15.0886C10.4727 23.2665 16.9771 29.896 25.0007 29.896Z"
+        fill="#7497F7"/>
+    <path
+        d="M23.7758 59.3444C14.3081 53.1885 6.39115 44.7034 0.759503 34.804C0.596267 34.5545 0.514648 34.3049 0.514648 33.9722C0.514648 33.3067 0.841121 32.7244 1.41245 32.4748L12.4309 26.9844C13.1655 26.5685 14.1449 26.9012 14.553 27.6499C20.103 37.1333 28.3464 44.7866 38.2222 49.4451C34.5494 53.1885 30.5501 56.516 26.2243 59.3444C25.4898 59.8435 24.5104 59.8435 23.7758 59.3444Z"
+        fill="#7497F7"/>
+    <path
+        d="M26.2247 59.3444C35.774 53.1885 43.6094 44.7034 49.241 34.804C49.4043 34.5545 49.4859 34.3049 49.4859 33.9722C49.4859 33.3067 49.1594 32.7244 48.5881 32.4748L37.5696 26.9844C36.8351 26.5685 35.8557 26.9012 35.4476 27.6499C29.8975 37.1333 21.6541 44.7866 11.7783 49.4451C15.3695 53.1885 19.4504 56.516 23.7762 59.3444C24.5107 59.8435 25.4902 59.8435 26.2247 59.3444Z"
+        fill="#7497F7"/>
+</svg>
diff --git a/config-ui/src/plugins/register/opsgenie/config.tsx b/config-ui/src/plugins/register/opsgenie/config.tsx
new file mode 100644
index 000000000..66ea2075e
--- /dev/null
+++ b/config-ui/src/plugins/register/opsgenie/config.tsx
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { ExternalLink } from '@/components';
+
+import Icon from './assets/icon.svg';
+import { Endpoint } from './connection-fields';
+
+export const OpsgenieConfig = {
+  plugin: 'opsgenie',
+  name: 'Opsgenie',
+  icon: Icon,
+  sort: 8,
+  connection: {
+    docLink: 'TODO',
+    initialValues: {
+      endpoint: 'https://api.opsgenie.com/',
+    },
+    fields: [
+      'name',
+      ({ initialValues, values, setValues }: any) => (
+        <Endpoint
+          initialValue={initialValues.endpoint ?? ''}
+          value={values.endpoint ?? ''}
+          setValue={(value) => setValues({ endpoint: value })}
+        />
+      ),
+      {
+        key: 'token',
+        label: 'Opsgenie API Key',
+        subLabel: <ExternalLink link={'TODO'}>Learn how to create a Atlassian Opsgenie personal API Key</ExternalLink>,
+      },
+      'proxy',
+      {
+        key: 'rateLimitPerHour',
+        subLabel:
+          'By default, DevLake uses 6,000 requests/hour for data collection for Opsgenie. But you can adjust the collection speed by setting up your desirable rate limit.',
+        learnMore: 'TODO',
+        externalInfo: 'Opsgenie rate limit is based on number of users and domains.',
+        defaultValue: 6000,
+      },
+    ],
+  },
+  dataScope: {
+    title: 'Opsgenie Services *',
+  },
+};
diff --git a/config-ui/src/plugins/register/opsgenie/connection-fields/endpoint.tsx b/config-ui/src/plugins/register/opsgenie/connection-fields/endpoint.tsx
new file mode 100644
index 000000000..a91857929
--- /dev/null
+++ b/config-ui/src/plugins/register/opsgenie/connection-fields/endpoint.tsx
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useEffect } from 'react';
+import { FormGroup, RadioGroup, Radio } from '@blueprintjs/core';
+
+import * as S from './styled';
+
+interface Props {
+  initialValue: string;
+  value: string;
+  setValue: (value: string) => void;
+}
+
+export const Endpoint = ({ initialValue, value, setValue }: Props) => {
+  useEffect(() => {
+    setValue(initialValue);
+  }, [initialValue]);
+
+  return (
+    <FormGroup
+      label={<S.Label>Opsgenie Instance Location (us or eu)</S.Label>}
+      labelInfo={<S.LabelInfo>*</S.LabelInfo>}
+    >
+      <RadioGroup
+        inline
+        selectedValue={value || initialValue}
+        onChange={(e) => {
+          setValue((e.target as any).value);
+        }}
+      >
+        <Radio value="https://api.opsgenie.com/">United States</Radio>
+        <Radio value="https://api.eu.opsgenie.com/">Europe</Radio>
+      </RadioGroup>
+    </FormGroup>
+  );
+};
diff --git a/config-ui/src/plugins/register/opsgenie/connection-fields/index.ts b/config-ui/src/plugins/register/opsgenie/connection-fields/index.ts
new file mode 100644
index 000000000..5c05f6971
--- /dev/null
+++ b/config-ui/src/plugins/register/opsgenie/connection-fields/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './endpoint';
diff --git a/config-ui/src/plugins/register/opsgenie/connection-fields/styled.ts b/config-ui/src/plugins/register/opsgenie/connection-fields/styled.ts
new file mode 100644
index 000000000..2fce9d014
--- /dev/null
+++ b/config-ui/src/plugins/register/opsgenie/connection-fields/styled.ts
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { Colors } from '@blueprintjs/core';
+import styled from 'styled-components';
+
+export const Label = styled.label`
+  font-size: 16px;
+  font-weight: 600;
+`;
+
+export const LabelInfo = styled.i`
+  color: #ff8b8b;
+`;
+
+export const LabelDescription = styled.p`
+  margin: 0;
+`;
+
+export const Endpoint = styled.div`
+  p {
+    margin: 10px 0;
+  }
+`;
+
+export const Input = styled.div`
+  margin-bottom: 8px;
+
+  .input {
+    display: flex;
+    align-items: center;
+
+    .info {
+      margin-left: 4px;
+
+      span.error {
+        color: ${Colors.RED3};
+      }
+
+      span.success {
+        color: ${Colors.GREEN3};
+      }
+    }
+  }
+
+  .warning {
+    margin-top: 8px;
+  }
+`;
+
+export const Alert = styled.div`
+  margin-top: 8px;
+  padding: 12px 20px;
+  background: #f9f9fa;
+  border: 1px solid #dbdcdf;
+  border-radius: 4px;
+`;
diff --git a/config-ui/src/plugins/register/opsgenie/index.ts b/config-ui/src/plugins/register/opsgenie/index.ts
new file mode 100644
index 000000000..de415db39
--- /dev/null
+++ b/config-ui/src/plugins/register/opsgenie/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './config';
diff --git a/grafana/dashboards/Opsgenie.json b/grafana/dashboards/Opsgenie.json
new file mode 100644
index 000000000..05c3c06ce
--- /dev/null
+++ b/grafana/dashboards/Opsgenie.json
@@ -0,0 +1,1454 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": {
+          "type": "datasource",
+          "uid": "grafana"
+        },
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "fiscalYearStartMonth": 0,
+  "graphTooltip": 0,
+  "id": 11,
+  "links": [
+    {
+      "asDropdown": false,
+      "icon": "bolt",
+      "includeVars": false,
+      "keepTime": true,
+      "tags": [],
+      "targetBlank": false,
+      "title": "Homepage",
+      "tooltip": "",
+      "type": "link",
+      "url": "/grafana/d/Lv1XbLHnk/data-specific-dashboards-homepage"
+    },
+    {
+      "asDropdown": false,
+      "icon": "external link",
+      "includeVars": false,
+      "keepTime": true,
+      "tags": [
+        "Data Source Specific Dashboard"
+      ],
+      "targetBlank": false,
+      "title": "Metric dashboards",
+      "tooltip": "",
+      "type": "dashboards",
+      "url": ""
+    }
+  ],
+  "liveNow": false,
+  "panels": [
+    {
+      "datasource": {
+        "type": "datasource",
+        "uid": "grafana"
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 13,
+        "x": 0,
+        "y": 0
+      },
+      "id": 128,
+      "options": {
+        "code": {
+          "language": "plaintext",
+          "showLineNumbers": false,
+          "showMiniMap": false
+        },
+        "content": "- Use Cases: This dashboard shows the incident data from Opsgenie.\n- Data Source Required: Opsgenie",
+        "mode": "markdown"
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": {
+            "type": "datasource",
+            "uid": "grafana"
+          },
+          "queryType": "randomWalk",
+          "refId": "A"
+        }
+      ],
+      "title": "Dashboard Introduction",
+      "type": "text"
+    },
+    {
+      "collapsed": false,
+      "datasource": {
+        "type": "datasource",
+        "uid": "grafana"
+      },
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 3
+      },
+      "id": 126,
+      "panels": [],
+      "targets": [
+        {
+          "datasource": {
+            "type": "datasource",
+            "uid": "grafana"
+          },
+          "refId": "A"
+        }
+      ],
+      "title": "1. Incident Resolution Status",
+      "type": "row"
+    },
+    {
+      "datasource": "mysql",
+      "description": "1. Total number of incidents created.\n2. The requirements being calculated are filtered by \"requirement creation time\" (time filter at the upper-right corner) and \"Opsgenie board\" (\"Choose Board\" filter at the upper-left corner)",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 4,
+        "x": 0,
+        "y": 4
+      },
+      "id": 114,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "area",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "text": {},
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "queryType": "randomWalk",
+          "rawQuery": true,
+          "rawSql": "select count(distinct i.issue_key) as value\r\nfrom issues i\r\n  join board_issues bi on i.id = bi.issue_id\r\nwhere \r\n  $__timeFilter(i.created_date)\r\n  and bi.board_id in ($board_id)\r\n",
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "value"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "timeColumn": "time",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "Number of Incidents [Created in Selected Time Range]",
+      "type": "stat"
+    },
+    {
+      "datasource": "mysql",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          },
+          "unit": "none"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 4,
+        "x": 4,
+        "y": 4
+      },
+      "id": 116,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "area",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "text": {},
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "queryType": "randomWalk",
+          "rawQuery": true,
+          "rawSql": "select count(distinct i.issue_key) as value\r\nfrom issues i\r\n  join board_issues bi on i.id = bi.issue_id\r\nwhere \r\n  i.original_status = 'resolved'\r\n  and $__timeFilter(i.created_date)\r\n  and bi.board_id in ($board_id)\r\n",
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "value"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "timeColumn": "time",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "Number of Resolved Incidents [Created in Selected Time Range]",
+      "type": "stat"
+    },
+    {
+      "datasource": "mysql",
+      "description": "1. Total number of incidents created.\n2. The requirements being calculated are filtered by \"requirement creation time\" (time filter at the upper-right corner) and \"Jira board\" (\"Choose Board\" filter at the upper-left corner)",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "custom": {
+            "align": "auto",
+            "cellOptions": {
+              "type": "auto"
+            },
+            "inspect": false
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byFrameRefID",
+              "options": "A"
+            },
+            "properties": [
+              {
+                "id": "custom.filterable",
+                "value": true
+              }
+            ]
+          },
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "title"
+            },
+            "properties": [
+              {
+                "id": "custom.width",
+                "value": 246
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 16,
+        "x": 8,
+        "y": 4
+      },
+      "id": 131,
+      "options": {
+        "cellHeight": "sm",
+        "footer": {
+          "countRows": false,
+          "fields": "",
+          "reducer": [
+            "sum"
+          ],
+          "show": false
+        },
+        "showHeader": true,
+        "sortBy": []
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "queryType": "randomWalk",
+          "rawQuery": true,
+          "rawSql": "select \r\n  i.title as \"Title\",\r\n  i.issue_key as \"Id\",\r\n  group_concat(b.name separator \", \") as \"Service(s)\",\r\n  i.description as \"Description\",\r\n  i.original_status as \"Original Status\",\r\n  i.priority as \"Priority\",\r\n  i.created_date as \"Created Date\",\r\n  i.updated_date as \"Updated Date\",\r\n  round((i.lead_time_minutes/1440),1) as \"Lead Time Days\",\r\n  i.url as \"URL\"\r\nfrom issues i\r\n  join board_issues bi on i.id = bi.iss [...]
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "value"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "timeColumn": "time",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "List of Incidents [Created in Selected Time Range]",
+      "type": "table"
+    },
+    {
+      "datasource": "mysql",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "text",
+                "value": null
+              }
+            ]
+          },
+          "unit": "none"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 4,
+        "x": 4,
+        "y": 7
+      },
+      "id": 134,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "area",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "text": {},
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "queryType": "randomWalk",
+          "rawQuery": true,
+          "rawSql": "select count(distinct i.issue_key) as value\r\nfrom issues i\r\n  join board_issues bi on i.id = bi.issue_id\r\nwhere \r\n  i.original_status = 'closed'\r\n  and $__timeFilter(i.created_date)\r\n  and bi.board_id in ($board_id)\r\n",
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "value"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "timeColumn": "time",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "Number of Closed Incidents [Created in Selected Time Range]",
+      "type": "stat"
+    },
+    {
+      "datasource": "mysql",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "red",
+                "value": null
+              },
+              {
+                "color": "green",
+                "value": 0.8
+              }
+            ]
+          },
+          "unit": "percentunit"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 8,
+        "x": 0,
+        "y": 10
+      },
+      "id": 117,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "area",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "text": {},
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "time_series",
+          "group": [],
+          "metricColumn": "none",
+          "queryType": "randomWalk",
+          "rawQuery": true,
+          "rawSql": "with _requirements as(\r\n  select\r\n    count(distinct i.id) as total_count,\r\n    count(distinct case when i.original_status = 'resolved' then i.id else null end) as resolved_count\r\n  from issues i\r\n    join board_issues bi on i.id = bi.issue_id\r\n  where \r\n    $__timeFilter(i.created_date)\r\n    and bi.board_id in ($board_id)\r\n)\r\n\r\nselect \r\n  now() as time,\r\n  1.0 * resolved_count/total_count as requirement_delivery_rate\r\nfrom _requirements",
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "value"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "timeColumn": "time",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "Incident Resolution Rate [Incidents created in the selected time range]",
+      "type": "stat"
+    },
+    {
+      "datasource": "mysql",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "Resolution Rate(%)",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 12,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "red",
+                "value": null
+              },
+              {
+                "color": "green",
+                "value": 0.8
+              }
+            ]
+          },
+          "unit": "percentunit"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 16,
+        "x": 8,
+        "y": 10
+      },
+      "id": 121,
+      "links": [],
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "pluginVersion": "8.0.6",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "time_series",
+          "group": [],
+          "metricColumn": "none",
+          "queryType": "randomWalk",
+          "rawQuery": true,
+          "rawSql": "with _requirements as(\r\n  select\r\n    DATE_ADD(date(i.created_date), INTERVAL -DAYOFMONTH(date(i.created_date))+1 DAY) as time,\r\n    1.0 * count(distinct case when i.original_status = 'resolved' then i.id else null end)/count(distinct i.id) as resolved_rate\r\n  from issues i\r\n    join board_issues bi on i.id = bi.issue_id\r\n  where \r\n    $__timeFilter(i.created_date)\r\n    and bi.board_id in ($board_id)\r\n  group by 1\r\n)\r\n\r\nselect\r\n  time,\r\n   [...]
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "value"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "timeColumn": "time",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "Incident Resolution Rate over Time [Incidents Created in Selected Time Range]",
+      "type": "timeseries"
+    },
+    {
+      "collapsed": false,
+      "datasource": {
+        "type": "datasource",
+        "uid": "grafana"
+      },
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 16
+      },
+      "id": 110,
+      "panels": [],
+      "targets": [
+        {
+          "datasource": {
+            "type": "datasource",
+            "uid": "grafana"
+          },
+          "refId": "A"
+        }
+      ],
+      "title": "2. Mean Time to Resolve (MTTR)",
+      "type": "row"
+    },
+    {
+      "datasource": "mysql",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "decimals": 1,
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "#EAB839",
+                "value": 1
+              },
+              {
+                "color": "red",
+                "value": 3
+              }
+            ]
+          },
+          "unit": "d"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 4,
+        "x": 0,
+        "y": 17
+      },
+      "id": 12,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "area",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "mean"
+          ],
+          "fields": "/^value$/",
+          "values": false
+        },
+        "text": {},
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "rawQuery": true,
+          "rawSql": "select \r\n  avg(lead_time_minutes/1440) as value\r\nfrom issues i\r\n  join board_issues bi on i.id = bi.issue_id\r\nwhere \r\n  i.original_status = 'resolved'\r\n  and $__timeFilter(i.resolution_date)\r\n  and bi.board_id in ($board_id)",
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "progress"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "table": "ca_analysis",
+          "timeColumn": "create_time",
+          "timeColumnType": "timestamp",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "MTTR [Incidents Resolved in Select Time Range]",
+      "type": "stat"
+    },
+    {
+      "datasource": "mysql",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "#EAB839",
+                "value": 1
+              },
+              {
+                "color": "red",
+                "value": 3
+              }
+            ]
+          },
+          "unit": "d"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 4,
+        "x": 4,
+        "y": 17
+      },
+      "id": 13,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "area",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "text": {},
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "rawQuery": true,
+          "rawSql": "with _ranks as(\r\n  select \r\n    i.lead_time_minutes,\r\n    percent_rank() over (order by lead_time_minutes asc) as ranks\r\n  from issues i\r\n    join board_issues bi on i.id = bi.issue_id\r\n  where \r\n    i.original_status = 'resolved'\r\n    and $__timeFilter(i.resolution_date)\r\n    and bi.board_id in ($board_id)\r\n)\r\n\r\nselect\r\n  max(lead_time_minutes/1440) as value\r\nfrom _ranks\r\nwhere \r\n  ranks <= 0.8",
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "progress"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "table": "ca_analysis",
+          "timeColumn": "create_time",
+          "timeColumnType": "timestamp",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "80% Incidents' MTTR are less than # [Incidents Resolved in Select Time Range]",
+      "type": "stat"
+    },
+    {
+      "datasource": "mysql",
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "Incident Age(days)",
+            "axisPlacement": "auto",
+            "axisSoftMin": 0,
+            "fillOpacity": 80,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineWidth": 1,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 16,
+        "x": 8,
+        "y": 17
+      },
+      "id": 17,
+      "interval": "",
+      "options": {
+        "barRadius": 0,
+        "barWidth": 0.5,
+        "fullHighlight": false,
+        "groupWidth": 0.7,
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "orientation": "auto",
+        "showValue": "auto",
+        "stacking": "none",
+        "text": {
+          "valueSize": 12
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        },
+        "xTickLabelRotation": 0,
+        "xTickLabelSpacing": 0
+      },
+      "pluginVersion": "8.0.6",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "rawQuery": true,
+          "rawSql": "with _requirements as(\r\n  select \r\n    DATE_ADD(date(i.resolution_date), INTERVAL -DAYOFMONTH(date(i.resolution_date))+1 DAY) as time,\r\n    avg(lead_time_minutes/1440) as mean_incident_age\r\n  from issues i\r\n    join board_issues bi on i.id = bi.issue_id\r\n  where \r\n    i.original_status = 'resolved'\r\n    and $__timeFilter(i.resolution_date)\r\n    and bi.board_id in ($board_id)\r\n  group by 1\r\n)\r\n\r\nselect \r\n  date_format(time,'%M %Y') as month [...]
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "progress"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "table": "ca_analysis",
+          "timeColumn": "create_time",
+          "timeColumnType": "timestamp",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "title": "Mean MTTR [Incidents Resolved in Select Time Range]",
+      "type": "barchart"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "mysql",
+      "description": "1. The cumulative distribution of MTTR\n2. Each point refers to the percent rank of a distinct duration to resolve incidents.",
+      "fill": 0,
+      "fillGradient": 4,
+      "gridPos": {
+        "h": 6,
+        "w": 24,
+        "x": 0,
+        "y": 23
+      },
+      "hiddenSeries": false,
+      "id": 15,
+      "legend": {
+        "alignAsTable": false,
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "rightSide": false,
+        "show": false,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 8,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": false
+      },
+      "percentage": false,
+      "pluginVersion": "9.5.2",
+      "pointradius": 0.5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "time_series",
+          "group": [],
+          "metricColumn": "none",
+          "rawQuery": true,
+          "rawSql": "with _ranks as(\r\n  select \r\n    round(i.lead_time_minutes/1440) as lead_time_day\r\n  from issues i\r\n    join board_issues bi on i.id = bi.issue_id\r\n  where \r\n    i.original_status = 'resolved'\r\n    and $__timeFilter(i.resolution_date)\r\n    and bi.board_id in ($board_id)\r\n  order by lead_time_day asc\r\n)\r\n\r\nselect \r\n  now() as time,\r\n  lpad(concat(lead_time_day,'d'), 4, ' ') as metric,\r\n  percent_rank() over (order by lead_time_day asc) as  [...]
+          "refId": "A",
+          "select": [
+            [
+              {
+                "params": [
+                  "progress"
+                ],
+                "type": "column"
+              }
+            ]
+          ],
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          },
+          "table": "ca_analysis",
+          "timeColumn": "create_time",
+          "timeColumnType": "timestamp",
+          "where": [
+            {
+              "name": "$__timeFilter",
+              "params": [],
+              "type": "macro"
+            }
+          ]
+        }
+      ],
+      "thresholds": [
+        {
+          "$$hashKey": "object:469",
+          "colorMode": "ok",
+          "fill": true,
+          "line": true,
+          "op": "lt",
+          "value": 0.8,
+          "yaxis": "right"
+        }
+      ],
+      "timeRegions": [],
+      "title": "Cumulative Distribution of MTTR [Incidents Resolved in Select Time Range]",
+      "tooltip": {
+        "shared": false,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "transformations": [],
+      "type": "graph",
+      "xaxis": {
+        "mode": "series",
+        "show": true,
+        "values": [
+          "current"
+        ]
+      },
+      "yaxes": [
+        {
+          "$$hashKey": "object:76",
+          "format": "percentunit",
+          "label": "Percent Rank (%)",
+          "logBase": 1,
+          "max": "1.2",
+          "show": true
+        },
+        {
+          "$$hashKey": "object:77",
+          "format": "short",
+          "logBase": 1,
+          "show": false
+        }
+      ],
+      "yaxis": {
+        "align": false
+      }
+    },
+    {
+      "datasource": {
+        "type": "datasource",
+        "uid": "grafana"
+      },
+      "gridPos": {
+        "h": 2,
+        "w": 24,
+        "x": 0,
+        "y": 29
+      },
+      "id": 130,
+      "options": {
+        "code": {
+          "language": "plaintext",
+          "showLineNumbers": false,
+          "showMiniMap": false
+        },
+        "content": "<br/>\n\nThis dashboard is created based on this [data schema](https://devlake.apache.org/docs/DataModels/DevLakeDomainLayerSchema). Want to add more metrics? Please follow the [guide](https://devlake.apache.org/docs/Configuration/Dashboards/GrafanaUserGuide).",
+        "mode": "markdown"
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": {
+            "type": "datasource",
+            "uid": "grafana"
+          },
+          "queryType": "randomWalk",
+          "refId": "A"
+        }
+      ],
+      "type": "text"
+    },
+    {
+      "collapsed": false,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 31
+      },
+      "id": 132,
+      "panels": [],
+      "title": "3. Incident Responders",
+      "type": "row"
+    },
+    {
+      "datasource": "mysql",
+      "description": "Due to Opsgenie API limitation regarding the relationship between assignee, users, and incident (and lack of 'assign at' key ), the column assignee represents a JSON object containing all assignee from said incident",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "custom": {
+            "align": "auto",
+            "cellOptions": {
+              "type": "json-view"
+            },
+            "inspect": false
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "issue_id"
+            },
+            "properties": [
+              {
+                "id": "custom.width",
+                "value": 504
+              }
+            ]
+          },
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "name"
+            },
+            "properties": [
+              {
+                "id": "custom.width",
+                "value": 286
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 24,
+        "x": 0,
+        "y": 32
+      },
+      "id": 133,
+      "options": {
+        "cellHeight": "sm",
+        "footer": {
+          "countRows": false,
+          "fields": "",
+          "reducer": [
+            "sum"
+          ],
+          "show": false
+        },
+        "showHeader": true,
+        "sortBy": []
+      },
+      "pluginVersion": "9.5.2",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "rawQuery": true,
+          "rawSql": "select \n  i.title as \"Incident Name\",\n  concat('[', group_concat(concat('{\"name\": \"', a.assignee_name, '\"', ', \"type\": \"', r.type, '\"}') separator \", \"), ']') as \"Responders\"\nfrom issues as i\n  inner join issue_assignees as a on (substring_index(i.id, \":\", -1) = substring_index(a.issue_id, \":\", -1))\n  inner join _tool_opsgenie_responders as r on a.assignee_id = r.id\nwhere i.id like 'opsgenie%'\ngroup by i.title;",
+          "refId": "A",
+          "sql": {
+            "columns": [
+              {
+                "parameters": [],
+                "type": "function"
+              }
+            ],
+            "groupBy": [
+              {
+                "property": {
+                  "type": "string"
+                },
+                "type": "groupBy"
+              }
+            ],
+            "limit": 50
+          }
+        }
+      ],
+      "title": "List of Incident Responder [organized in JSON format]",
+      "type": "table"
+    }
+  ],
+  "refresh": false,
+  "schemaVersion": 38,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": [
+      {
+        "current": {
+          "selected": true,
+          "text": [
+            "All"
+          ],
+          "value": [
+            "$__all"
+          ]
+        },
+        "datasource": "mysql",
+        "definition": "select concat(name, '--', id) from boards where id like 'opsgenie%'",
+        "hide": 0,
+        "includeAll": true,
+        "label": "Choose Board",
+        "multi": true,
+        "name": "board_id",
+        "options": [],
+        "query": "select concat(name, '--', id) from boards where id like 'opsgenie%'",
+        "refresh": 1,
+        "regex": "/^(?<text>.*)--(?<value>.*)$/",
+        "skipUrlSync": false,
+        "sort": 0,
+        "type": "query"
+      }
+    ]
+  },
+  "time": {
+    "from": "2021-06-05T21:00:42.390Z",
+    "to": "2025-06-16T21:00:42.390Z"
+  },
+  "timepicker": {},
+  "timezone": "",
+  "title": "Opsgenie",
+  "uid": "b4556439-f173-4411-93d4-65f261726d24",
+  "version": 2,
+  "weekStart": ""
+}