Google Cloud와 고객 관계 관리(CRM) 시스템(예: Jira Service Desk, Zendesk, ServiceNow) 간의 지원 케이스를 통합할 커넥터를 빌드하여 이러한 시스템을 동기화할 수 있습니다.
이 커넥터에서는 Customer Care의 Cloud Support API(CSAPI)를 사용합니다. 이 문서에서는 커넥터를 빌드하고 사용하는 방법의 예시를 제공합니다. 사용 사례에 맞게 디자인을 조정할 수 있습니다.
가정
CRM 작동 방식과 커넥터를 작성하는 언어에 대한 몇 가지 중요한 가정이 있습니다. CRM의 기능이 서로 다른 경우에도 완벽하게 작동하는 커넥터를 빌드할 수 있지만 이 가이드에서 수행하는 방식과 다른 방식으로 구현해야 할 수도 있습니다.
이 가이드는 다음과 같은 가정을 기반으로 작성되었습니다.
- Python 및 Flask 마이크로 프레임워크로 커넥터를 빌드합니다.
- 소규모 앱을 빌드할 수 있는 간편한 프레임워크인 Flask를 사용한다고 가정합니다. 자바와 같은 다른 언어나 프레임워크도 사용할 수 있습니다.
- 연결, 댓글, 우선순위, 케이스 메타데이터, 케이스 상태를 동기화하려고 합니다. 원하지 않는 한 모든 데이터를 동기화할 필요는 없습니다. 예를 들어 연결을 동기화하지 않으려면 동기화하지 마세요.
- CRM은 동기화하려는 필드를 읽고 쓸 수 있는 엔드포인트를 노출합니다. 이 가이드에서와 같이 모든 필드를 동기화하려면 CRM의 엔드포인트에서 다음 작업을 지원하는지 확인합니다.
작업 CSAPI 상응 변경되지 않는 일부 정적 ID를 사용하는 케이스를 가져옵니다. cases.get
케이스를 만듭니다. cases.create
케이스를 종료합니다. cases.close
케이스 우선순위를 업데이트합니다. cases.patch
케이스의 연결을 나열합니다. cases.attachments.list
케이스에 연결을 다운로드합니다. media.download
케이스에 연결을 업로드합니다. media.upload
케이스에 댓글을 나열합니다. cases.comments.list
케이스에 새 댓글을 추가합니다. cases.comments.create
케이스를 검색합니다.* cases.search
*마지막 업데이트 시간을 기준으로 필터링할 수 있어야 합니다. 또한 Customer Care에 동기화할 케이스를 결정하는 방법이 있어야 합니다. 예를 들어 CRM의 케이스에 커스텀 필드가 포함될 수 있는 경우 synchronizeWithGoogleCloudSupport
라는 커스텀 불리언 필드를 채우고 이를 기반으로 필터링할 수 있습니다.
대략적인 디자인
커넥터는 전적으로 Google Cloud 제품과 CRM을 통해 빌드됩니다. Flask 마이크로 프레임워크에서 Python을 실행하는 App Engine 앱입니다. Cloud Tasks를 사용하여 새 케이스에 CSAPI와 CRM을 주기적으로 폴하고 기존 케이스에 업데이트하며 케이스 간에 변경사항을 동기화합니다. 케이스에 대한 일부 메타데이터는 Firestore에 저장되지만 더 이상 필요하지 않으면 삭제됩니다.
다음 다이어그램에서는 대략적인 디자인을 보여줍니다.
커넥터 목표
커넥터의 기본 목표는 동기화하려는 케이스가 CRM에 생성될 때 해당 케이스가 Customer Care에서 생성되고 케이스의 모든 후속 업데이트가 케이스 간에 동기화되는 것입니다. 마찬가지로 케이스가 Customer Care에서 생성되면 케이스를 CRM과 동기화해야 합니다.
특히 다음 케이스 관점을 동기화해야 합니다.
- 케이스 작성:
- 한 시스템에서 케이스가 생성되면 커넥터가 다른 시스템에 해당 케이스를 만들어야 합니다.
- 시스템 하나를 사용할 수 없는 경우 시스템을 사용할 수 있게 되면 시스템에서 케이스를 만들어야 합니다.
- 댓글:
- 한 시스템의 케이스에 댓글을 추가하는 경우 다른 시스템의 해당 케이스에 댓글을 추가해야 합니다.
- 연결:
- 한 시스템의 케이스에 연결이 추가되면 다른 시스템의 해당 케이스에 연결을 추가해야 합니다.
- 우선순위:
- 한 시스템에서 케이스 우선순위를 업데이트하면 다른 시스템에서 해당 케이스의 우선순위를 업데이트해야 합니다.
- 케이스 상태:
- 한 시스템에서 케이스가 종료되면 다른 시스템에서도 케이스를 종료해야 합니다.
인프라
Google Cloud 제품
커넥터는 동기화된 케이스에 대한 데이터를 저장하기 위해 Datastore 모드에서 구성된 Cloud Firestore를 사용하는 App Engine 앱입니다. Cloud Tasks를 사용하여 자동 재시도 로직으로 태스크를 예약합니다.
커넥터는 Customer Care에 액세스하기 위해 서비스 계정을 사용하여 V2 Cloud Support API를 호출합니다. 인증을 위해 서비스 계정에 적절한 권한을 부여해야 합니다.
CRM
커넥터는 개발자가 제공한 메커니즘을 사용하여 CRM의 케이스에 액세스합니다. CRM에서 노출한 API를 호출한다고 가정합니다.
조직의 보안 고려사항
커넥터는 커넥터를 빌드하는 조직과 조직의 모든 하위 프로젝트에 있는 케이스를 동기화합니다. 이렇게 하면 해당 조직의 사용자가 액세스하지 않으려는 고객 지원 데이터에 액세스할 수 있습니다. 조직의 보안을 유지하기 위해 IAM 역할을 구조화하는 방법을 신중하게 고려하세요.
세부 설계
CSAPI 설정
CSAPI를 설정하려면 다음 단계를 수행합니다.
- 조직의 Cloud Customer Care 지원 서비스를 구매합니다.
- 커넥터를 실행하려는 프로젝트에서 Cloud Support API를 사용 설정합니다.
- 커넥터에서 사용할 기본 앱 프레임워크 서비스 계정의 사용자 인증 정보를 가져옵니다.
- 조직 수준에서 서비스 계정에 다음 역할을 부여합니다.
Tech Support Editor
Organization Viewer
CSAPI 설정에 대한 자세한 내용은 Cloud Support API V2 사용자 가이드를 참조하세요.
CSAPI 호출
Python을 사용하여 CSAPI를 호출합니다. Python으로 CSAPI를 호출하는 방법에는 다음 두 가지가 있습니다.
- proto에서 생성된 클라이언트 라이브러리. 이는 최신 및 관용적이지만 CSAPI의 연결 엔드포인트 호출을 지원하지는 않습니다. 자세한 내용은 GAPIC Generator를 참조하세요.
- 탐색 문서에서 생성된 클라이언트 라이브러리. 이전 버전이지만 연결을 지원합니다. 자세한 내용은 Google API 클라이언트를 참조하세요.
다음은 탐색 문서에서 생성된 클라이언트 라이브러리를 사용하여 CSAPI를 호출하는 예시입니다.
"""
Gets a support case using the Cloud Support API.
Before running, do the following:
- Set the GOOGLE_APPLICATION_CREDENTIALS environment variable to
your service account credentials.
- Install the Google API Python Client: https://github.com/googleapis/google-api-python-client
- Change NAME to point to a case that your service account has permission to get.
"""
import os
import googleapiclient.discovery
NAME = "projects/some-project/cases/43595344"
def main():
api_version = "v2"
supportApiService = googleapiclient.discovery.build(
serviceName="cloudsupport",
version=api_version,
discoveryServiceUrl=f"https://cloudsupport.googleapis.com/$discovery/rest?version={api_version}",
)
request = supportApiService.cases().get(
name=NAME,
)
print(request.execute())
if __name__ == "__main__":
main()
더 많은 CSAPI 호출 예시는 이 저장소를 참조하세요.
Google 리소스 이름, ID, 번호
organization_id
가 조직의 ID입니다. Customer Care에서 조직 또는 조직 내 프로젝트에 케이스를 만들 수 있습니다. project_id
는 케이스를 만들 수 있는 프로젝트의 이름입니다.
케이스 이름
케이스 이름은 다음과 같이 표시됩니다.
organizations/{organization_id}/cases/{case_number}
projects/{project_id}/cases/{case_number}
여기서 case_number
는 케이스에 할당된 번호입니다. 예를 들면 51234456
입니다.
댓글 이름
댓글 이름은 다음과 같이 표시됩니다.
organizations/{organization_id}/cases/{case_number}/comments/{comment_id}
여기서 comment_id
는 댓글에 할당된 번호입니다. 예를 들면 3
입니다. 또한 조직 외에 상위 프로젝트도 허용됩니다.
첨부파일 이름
연결 이름은 다음과 같습니다.
organizations/{organization_id}/cases/{case_number}/attachments/{attachment_id}
여기서 attachment_id
는 케이스의 연결 ID입니다(있는 경우). 예를 들면 0684M00000JvBpnQAF
입니다. 또한 조직 외에 상위 프로젝트도 허용됩니다.
Firestore 항목
CaseMapping
CaseMapping
은 케이스에 대한 메타데이터를 저장하도록 정의된 객체입니다.
동기화되거나 더 이상 필요하지 않으면 삭제되는 모든 케이스에 생성됩니다. Firebase에서 지원되는 데이터 유형에 대한 자세한 내용은 지원되는 데이터 유형을 참조하세요.
CaseMapping
에는 다음과 같은 속성이 있습니다.
속성 | 설명 | 유형 | 예 |
---|---|---|---|
ID |
기본 키입니다. CaseMapping 이 생성되면 Firestore에서 자동으로 할당합니다. |
정수 | 123456789 |
googleCaseName |
케이스의 전체 이름으로, 조직 또는 프로젝트 ID, 케이스 번호가 포함됩니다. | 텍스트 문자열 | organizations/123/cases/456 |
companyCaseID |
CRM의 케이스 ID입니다. | 정수 | 789 |
newContentAt |
Google 케이스 또는 CRM의 케이스에서 새 콘텐츠가 마지막으로 감지된 시간입니다. | 날짜 및 시간 | 0001-01-01T00:00:00Z |
resolvedAt |
Google 케이스가 해결된 시점의 타임스탬프입니다. 더 이상 필요하지 않을 때 CaseMappings 를 삭제하는 데 사용됩니다. |
날짜 및 시간 | 0001-01-01T00:00:00Z |
companyUpdatesSyncedAt |
커넥터에서 마지막으로 업데이트를 CRM에 성공적으로 폴링하고 Google 케이스에 업데이트를 동기화한 시간의 타임스탬프입니다. 서비스 중단 감지에 사용됩니다. | 날짜 및 시간 | 0001-01-01T00:00:00Z |
googleUpdatesSyncedAt |
커넥터에서 마지막으로 업데이트를 성공적으로 Google에 폴링하고 CRM 케이스에 대한 업데이트를 동기화한 시간의 타임스탬프입니다. 서비스 중단 감지에 사용됩니다. | 날짜 및 시간 | 0001-01-01T00:00:00Z |
outageCommentSentToGoogle |
서비스 중단이 감지된 경우 Google 케이스에 댓글이 추가되었는지 여부. 여러 서비스 중단 댓글이 추가되지 않도록 하는 데 사용됩니다. | 불리언 | False |
outageCommentSentToCompany |
서비스 중단이 감지된 경우 CRM 케이스에 댓글이 추가되었는지 여부. 여러 서비스 중단 댓글이 추가되지 않도록 하는 데 사용됩니다. | 불리언 | False |
priority |
케이스의 우선순위 수준입니다. | 정수 | 2 |
Global
Global
은 커넥터의 전역 변수를 저장하는 객체입니다.
Global
객체 하나만 생성되며 삭제되지 않습니다. 아키텍처의 형태는 다음과 같습니다.
속성 | 설명 | 유형 | 예 |
---|---|---|---|
ID | 기본 키입니다. 이 객체가 생성되면 Firestore에서 자동으로 할당합니다. | 정수 | 123456789 |
google_last_polled_at |
Customer Care에서 마지막으로 업데이트를 폴링한 시간입니다. | 날짜 및 시간 | 0001-01-01T00:00:00Z |
company_last_polled_at |
회사에서 마지막으로 업데이트를 폴링한 시간입니다. | 날짜 및 시간 | 0001-01-01T00:00:00Z |
Tasks
PollGoogleForUpdates
This task is scheduled to run every 60 seconds. It does the following:
- Search for recently updated cases:
- Call
CSAPI.SearchCases(organization_id, page_size=100, filter="update_time>{Global.google_last_polled_at - GOOGLE_POLLING_WINDOW}")
- Continue fetching pages as long as a
nextPageToken
is returned.
GOOGLE_POLLING_WINDOW
represents the period during which a case is continually checked for updates, even after it has been synced. The larger its value, the more tolerant the connector is to changes that are added while a case is syncing. We recommend that you set GOOGLE_POLLING_WINDOW
to 30 minutes to avoid any problems with comments being added out of order.
- Make a
CaseMapping
for any new cases:
- If
CaseMapping
does not exist for case.name
and case.create_time
is less than 30 days ago, then create a CaseMapping
with the following values:
Property
Value
caseMappingID
N/A
googleCaseName
case.name
companyCaseID
null
newContentAt
current_time
resolvedAt
null
companyUpdatesSyncedAt
current_time
googleUpdatesSyncedAt
null
outageCommentSentToGoogle
False
outageCommentSentToCompany
False
priority
case.priority
(converted to an integer)
- Queue tasks to sync all recently updated cases:
- Specifically,
SyncGoogleCaseToCompany(case.name)
.
- Update
CaseMappings
:
- For each open
CaseMapping
not recently updated, update CaseMapping.googleUpdatesSyncedAt
to the current time.
- Update last polled time:
- Update
Global.google_last_polled_at
in Firestore to the current time.
Retry logic
Configure this task to retry a few times within the first minute and then expire.
PollCompanyForUpdates
This task is scheduled to run every 60 seconds. It does the following:
- Search for recently updated cases:
- Call
YOUR_CRM.SearchCases(page_size=100, filter=”update_time>{Global.company_last_polled_at - COMPANY_POLLING_WINDOW} AND synchronizeWithGoogleCloudSupport=true”)
.
COMPANY_POLLING_WINDOW
can be set to whatever time duration works for you. For example, 5 minutes.
- Make a
CaseMapping
for any new cases:
- For each case, if
CaseMapping
does not exist for case.id
and case.create_time
is less than 30 days ago, create a CaseMapping
that looks like this:
Property
Value
caseMappingID
N/A
googleCaseName
null
companyCaseID
case.id
newContentAt
current_time
resolvedAt
null
companyUpdatesSyncedAt
null
googleUpdatesSyncedAt
current_time
outageCommentSentToGoogle
False
outageCommentSentToCompany
False
priority
case.priority
(converted to an integer)
- Queue tasks to sync all recently updated cases:
- Specifically, queue
SyncCompanyCaseToGoogle(case.name)
.
- Update
CaseMappings
:
- For each open
CaseMapping
not recently updated, update CaseMapping.companyUpdatesSyncedAt
to the current time.
- Update last polled time:
- Update
Global.company_last_polled_at
in Firestore to the current time.
Retry logic
Configure this task to retry a few times within the first minute and then expire.
SyncGoogleUpdatesToCompany(case_name)
Implementation
- Get the case and case mapping:
- Get
CaseMapping
for case_name
.
- Call
CSAPI.GetCase(case_name)
.
- If necessary, update resolved time and case status:
- If
CaseMapping.resolvedAt == null
and case.status == CLOSED
:
- Set
CaseMapping.resolvedAt
to case.update_time
- Close the case in the CRM as well
- Try to connect to an existing case in the CRM. If unable, then make a new one:
- If
CaseMapping.companyCaseID == null
:
- Try to get your CRM case with
custom_field_google_name == case_name
custom_field_google_name
is a custom field you create on the case object in your CRM.
- If the CRM case can't be found, call
YOUR_CRM.CreateCase(case)
with the following case:
Case field name in your CRM
Value
Summary
Case.diplay_name
Priority
Case.priority
Description
"CONTENT MIRRORED FROM GOOGLE SUPPORT:\n" + Case.description
Components
"Google Cloud
"
Customer Ticket (custom_field_google_name
)
case_name
Attachments
N/A
- Update the
CaseMapping
with a CaseMapping
that looks like this:
Property
Value
companyCaseID
new_case.id
googleUpdatesSyncedAt
current_time
- Add comment to Google case: "This case is now syncing with Company Case:
{case_id}
".
- Synchronize the comments:
- Get all comments:
- Call
CSAPI.ListComments(case_name, page_size=100)
. The maximum page size is 100. Continue retrieving successive pages until the oldest comment retrieved is older than googleUpdatesSyncedAt - GOOGLE_POLLING_WINDOW
.
- Call
YOUR_CRM.GetComments(case_id, page_size=50)
. Continue retrieving successive pages until the oldest comment retrieved is older than companyUpdatesSyncedAt - COMPANY_POLLING_WINDOW
.
- Optional: If you'd like, consider caching comments in some way so you can avoid making extra calls here. We leave the implementation of that up to you.
- Compare both lists of comments to determine if there are new comments on the Google Case.
- For each new Google comment:
- Call
YOUR_CRM.AddComment(comment.body)
, starting with "[Google Comment {comment_id}
by {comment.actor.display_name}
]".
- Repeat for attachments.
- Update
CaseMapping.googleUpdatesSyncedAt
to the current time.
Retry logic
Configure this task to retry indefinitely with exponential backoff.
SyncCompanyUpdatesToGoogle(case_id)
Implementation:
- Get the case and case mapping.
- Get
CaseMapping
for case.id
.
- Call
YOUR_CRM.GetCase(case.id)
.
- If necessary, update resolved time and case status:
- If
CaseMapping.resolvedAt == null
and case.status == CLOSED
:
- Set
CaseMapping.resolvedAt
to case.update_time
- Close the case in CSAPI as well
- Try to connect to an existing case in CSAPI. If unable, then make a new one:
- If
CaseMapping.googleCaseName == null
:
- Search through cases in CSAPI. Try to find a case that has a comment containing “This case is now syncing with Company Case:
{case_id}
”. If you're able to find one, then set googleCaseName
equal to its name.
- Otherwise, call
CSAPI.CreateCase(case)
:
- Synchronize the comments.
- Get all comments for the case from CSAPI and the CRM:
- Call
CSAPI.ListComments(case_name, page_size=100)
. Continue retrieving successive pages until the oldest comment retrieved is older than googleUpdatesSyncedAt - GOOGLE_POLLING_WINDOW
.
- Call
YOUR_CRM.GetComments(case_id, page_size=50)
. Continue retrieving successive pages until the oldest comment retrieved is older than companyUpdatesSyncedAt - COMPANY_POLLING_WINDOW
.
- NOTE: If you'd like, consider caching comments in some way so you can avoid making extra calls here. We leave the implementation of that up to you.
- Compare both lists of comments to determine if there are new comments on the CRM case.
- For each new Company comment:
- Call
CSAPI.AddComment
, starting with "[Company Comment {comment.id}
by {comment.author.displayName}
]".
- Repeat for attachments.
- Update
CaseMapping.companyUpdatesSyncedAt
to the current time.
Retry logic
Configure this task to retry indefinitely with exponential backoff.
CleanUpCaseMappings
This task is scheduled to run daily. It deletes any CaseMapping
for a case that has been closed for 30 days according to resolvedAt
.
Retry logic
Configure this task to retry with exponential backoff for up to 24 hours.
DetectOutages
This task is scheduled to run once every 5 minutes. It detects outages and alerts your Google and CRM cases (when possible) if a case is not syncing within the expected latency_tolerance
.
latency_tolerance
is defined as follows, where Time Since New Content = currentTime - newContentAt
:
Priority
Fresh (<1 hour)
Default (1 hour-1day)
Stale (>1 day)
P0
10 min
10 min
15 min
P1
10 min
15 min
60 min
P2
10 min
20 min
120 min
P3
10 min
20 min
240 min
P4
10 min
30 min
240 min
The latency that is relevant for the connector is not request latency, but rather the latency between when a change is made in one system and when it is propagated to the other. We make latency_tolerance
dependent on priority and freshness to avoid spamming cases unnecessarily. If there is a short outage, such as scheduled maintenance on either system, we don't need to alert P4
cases that haven't been updated recently.
When DetectOutages
runs, it does the following:
- Determine if a
CaseMapping
needs an outage comment, whereupon it adds one:
- For each
CaseMapping
in Firestore:
- If recently added (
companyCaseId
or googleUpdatesSyncedAt
is not defined), then ignore.
- If
current_time > googleUpdatesSyncedAt + latency_tolerance OR current_time > companyUpdatesSyncedAt + latency_tolerance
:
- If
!outageCommentSentToGoogle
:
- Try:
- Add comment to Google that "This case has not synced properly in
{duration since sync}
."
- Set
outageCommentSentToGoogle = True
.
- If
!outageCommentSentToCompany
:
- Try:
- Add comment to your CRM that "This case has not synced properly in
{duration since sync}
."
- Set
outageCommentSentToCompany = True
.
- Else:
- If
outageCommentSentToGoogle
:
- Try:
- Add comment to Google that "Syncing has resumed."
- Set
outageCommentSentToGoogle = False
.
- If
outageCommentSentToCompany
:
- Try:
- Add comment to your CRM that "Syncing has resumed."
- Set
outageCommentSentToCompany = False
.
- Return a failing status code (4xx or 5xx) if an outage is detected. This causes any monitoring you've set up to notice that there is a problem with the task.
Retry logic
Configure this task to retry a few times within the first 5 minutes and then expire.
What's next
Your connector is now ready to use.
If you'd like, you can also implement unit tests and integration tests. Also, you can add monitoring to check that the connector is working correctly on an ongoing basis.