Google Cloud と顧客管理(CRM)システム(Jira Service Desk、Zendesk、ServiceNow など)を統合するコネクタを構築することで、これらの間でサポートケースを同期できます。
このコネクタは、カスタマーケアの Cloud Support API(CSAPI)を使用します。このドキュメントでは、コネクタを作成して使用する方法の例を示します。ユースケースに合わせて設計を微調整できます。
前提条件
CRM の仕組みとコネクタの記述言語は重要な前提です。CRM の機能が異なる場合でも、問題なく動作するコネクタを構築できますが、このガイドとは異なる方法で実装する必要があります。
このガイドは、次のことを前提としています。
- Python と Flask のマイクロフレームワークを使用してコネクタを構築しています。
- Flask は、小さなアプリを構築するための簡単なフレームワークであるため、使用していることを前提としています。 Java などの他の言語やフレームワークを使用することもできます。
- 添付ファイル、コメント、優先度、ケースのメタデータ、ケースのステータスを同期する場合。必要な場合を除き、すべてのデータを同期する必要はありません。 たとえば、添付ファイルを同期したくない場合は、同期しないでください。
- 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
*最終更新時間でフィルタできる必要があります。また、どのケースがカスタマーケアに同期されるのかを判断する方法も必要です。たとえば、CRM のケースにカスタム フィールドを含める場合は、synchronizeWithGoogleCloudSupport
という名前のカスタムブール値フィールドに入力し、それに基づいてフィルタリングできます。
設計の概要
コネクタは、完全に Google Cloud プロダクトと CRM を使用して構築されています。これは、Flask マイクロフレームワークで Python を実行する App Engine アプリです。Cloud Tasks を使用して CSAPI と CRM を定期的に新しいケースと既存のケースの更新をポーリングし、それらのケース間で変更を同期します。ケースに関する一部のメタデータは Firestore に保存されますが、不要になった後に削除されます。
次の図は、その設計の概要を示しています。
コネクタの目標
コネクタの主な目的は、同期対象のケースが CRM で作成された場合、対応するケースがカスタマーケアに作成され、その後のすべてのケースの間でそれらの更新が同期されることです。同様に、カスタマーケアでケースが作成されたら、CRM に同期する必要があります。
具体的には、以下の側面を同期する必要があります。
- ケースの作成:
- 一方のシステムでケースを作成する際は、コネクタはもう一方のシステムで対応するケースを作成する必要があります。
- 1 つのシステムが使用できない場合は、ケースが使用可能になったときにケースを作成する必要があります。
- コメント:
- 一方のシステムでケースにコメントを追加する際は、もう一方のシステムの対応するケースにコメントを追加する必要があります。
- 添付ファイル:
- 一方のシステムでケースに添付ファイルを追加する際は、もう一方のシステムの対応するケースにも追加する必要があります。
- 優先度:
- 一方のシステムのケースの優先度を更新する際は、もう一方のシステムに対応するケースの優先度を更新する必要があります。
- ケースのステータス:
- 1 つのシステムでケースがクローズする際は、他のシステムでもクローズする必要があります。
インフラストラクチャ
Google Cloud プロダクト
このコネクタは、同期されたケースに関するデータを保存するために Datastore モードで構成された Cloud Firestore を使用する App Engine アプリです。Cloud Tasks を使用して自動化された再試行ロジックでジョブをスケジュールします。
カスタマーケアのケースにアクセスするには、コネクタでサービス アカウントを使用して V2 Cloud Support API を呼び出します。認証のために、サービス アカウントに適切な権限を付与する必要があります。
CRM
コネクタは、提供されるメカニズムを使用して CRM 内のケースにアクセスします。おそらく、CRM によって公開される API を呼び出します。
組織のセキュリティに関する考慮事項
コネクタは、ビルドを行う組織内のケースと組織のすべての子プロジェクトを同期します。これにより、その組織のユーザーが、アクセスさせたくないカスタマー サポート データにアクセスできるようになる可能性があります。組織内のセキュリティを維持するために、IAM ロールをどのように構造化するかを慎重に検討します。
詳細な設計
CSAPI を設定する
CSAPI を設定する手順は次のとおりです。
- 組織向けの Cloud カスタマーケア サポート サービスを購入します。
- コネクタを実行するプロジェクトに対して、Cloud Support API を有効にします。
- コネクタによって使用されるデフォルトの Apps Framework サービス アカウントの認証情報を取得します。
- 組織レベルで、サービス アカウントに次のロールを付与します。
Tech Support Editor
Organization Viewer
CSAPI の設定の詳細については、Cloud Support API V2 ユーザーガイドをご覧ください。
CSAPI の呼び出し
Python を使用して CSAPI を呼び出します。Python で CSAPI を呼び出すには、次の 2 つの方法をおすすめします。
- proto から生成されたクライアント ライブラリ。これらはより新しく、慣用的ですが、CSAPI のアタッチメント エンドポイントの呼び出しをサポートしていません。詳細については、GAPIC ジェネレータをご覧ください。
- ディスカバリ ドキュメントから生成されたクライアント ライブラリ。これらはより古いもののですが、アタッチメントをサポートしています。詳細については、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 にします。カスタマーケアで、組織内または組織内のプロジェクトの下でケースを作成できます。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
オブジェクトは 1 つだけ作成され、削除されません。たとえば、次のようになります。
プロパティ | 説明 | 型 | 例 |
---|---|---|---|
ID | 主キー。このオブジェクトの作成時に Firestore によって自動的に割り当てられます。 | 整数 | 123456789 |
google_last_polled_at |
カスタマーケアが最後にポーリングされた時刻。 | 日付と時刻 | 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.