Google Cloud Customer Care 케이스 동기화

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에 저장되지만 더 이상 필요하지 않으면 삭제됩니다.

다음 다이어그램에서는 대략적인 디자인을 보여줍니다.

커넥터에서 CSAPI 및 CRM을 호출합니다. App Engine 앱, Cloud Tasks, 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를 설정하려면 다음 단계를 수행합니다.

  1. 조직의 Cloud Customer Care 지원 서비스를 구매합니다.
  2. 커넥터를 실행하려는 프로젝트에서 Cloud Support API를 사용 설정합니다.
  3. 커넥터에서 사용할 기본 앱 프레임워크 서비스 계정의 사용자 인증 정보를 가져옵니다.
  4. 조직 수준에서 서비스 계정에 다음 역할을 부여합니다.
    • Tech Support Editor
    • Organization Viewer

CSAPI 설정에 대한 자세한 내용은 Cloud Support API V2 사용자 가이드를 참조하세요.

CSAPI 호출

Python을 사용하여 CSAPI를 호출합니다. Python으로 CSAPI를 호출하는 방법에는 다음 두 가지가 있습니다.

  1. proto에서 생성된 클라이언트 라이브러리. 이는 최신 및 관용적이지만 CSAPI의 연결 엔드포인트 호출을 지원하지는 않습니다. 자세한 내용은 GAPIC Generator를 참조하세요.
  2. 탐색 문서에서 생성된 클라이언트 라이브러리. 이전 버전이지만 연결을 지원합니다. 자세한 내용은 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.