É possível sincronizar os casos de suporte entre o Google Cloud e seu sistema de gestão de relacionamento com o cliente (CRM), como Jira Service Desk, Zendesk e ServiceNow, criando um conector para integrá-los.
Este conector usa a API Cloud Support (CSAPI) do Customer Care. Neste documento, mostramos um exemplo de como criar e usar um conector. É possível ajustar o design para se adequar ao seu caso de uso.
Suposições
Fazemos algumas suposições importantes sobre como seu CRM funciona e em que idioma você escreve o conector. Se seu CRM tiver recursos diferentes, você ainda poderá criar um conector que funcione perfeitamente, mas talvez seja necessário implementá-lo de uma maneira diferente da que fizemos neste guia.
Este guia é baseado nas seguintes suposições:
- Você está criando o conector com Python e a microframework Flask.
- Presumimos que você esteja usando o Flask porque ele é um framework fácil de criar um app pequeno. Também é possível usar outras linguagens ou frameworks, como o Java.
- Você quer sincronizar anexos, comentários, prioridade, metadados e status dos casos. Você não precisa sincronizar todos esses dados, a menos que queira. Por exemplo, não sincronize anexos se não quiser fazer isso.
- Seu CRM expõe endpoints capazes de ler e gravar os campos que você quer sincronizar. Se você quiser sincronizar todos os campos como fizemos neste guia, verifique se os endpoints do seu CRM são compatíveis com as seguintes operações:
Operação Equivalente de CSAPI Receba casos usando um ID estático e inalterável. cases.get
Criar casos. cases.create
Encerrar casos. cases.close
Atualizar a prioridade de um caso. cases.patch
Listar anexos em um caso. cases.attachments.list
Fazer o download de um anexo em um caso. media.download
Faça upload de um anexo em um caso. media.upload
Listar comentários em um caso. cases.comments.list
Adicionar um novo comentário em um caso. cases.comments.create
Pesquisar casos*. cases.search
*Você deve poder filtrar pelo horário da última atualização. Também é preciso haver uma maneira de determinar quais casos serão sincronizados com o atendimento ao cliente. Por exemplo, se os casos no seu CRM podem ter campos personalizados, é possível preencher um campo booleano personalizado chamado synchronizeWithGoogleCloudSupport
e filtrar com base nisso.
Design de alto nível
O conector é criado usando os produtos do Google Cloud e seu CRM. É um aplicativo do App Engine que executa Python na microestrutura Flask. Ele usa o Cloud Tasks para pesquisar periodicamente a CSAPI e seu CRM em busca de novos casos e atualizações de casos existentes, além de sincronizar alterações entre eles. Alguns metadados sobre casos são armazenados no Firestore, mas são excluídos quando não são mais necessários.
O diagrama a seguir mostra o design geral:
Metas do conector
O principal objetivo do conector é que, quando um caso que você quer sincronizar for criado no seu CRM, um caso correspondente seja criado no Atendimento ao cliente e todas as atualizações subsequentes no caso sejam sincronizadas entre eles. Da mesma forma, quando um caso é criado no atendimento ao cliente, ele precisa ser sincronizado com seu CRM.
Especificamente, os seguintes aspectos dos casos precisam ser sincronizados:
- Criação de casos:
- Quando um caso é criado em um sistema, o conector precisa criar um caso correspondente no outro.
- Se um sistema não estiver disponível, o caso precisará ser criado nele.
- Comentários:
- Quando um comentário é adicionado a um caso em um sistema, ele precisa ser adicionado ao caso correspondente em outro sistema.
- Anexos:
- Quando um anexo é adicionado a um caso em um sistema, ele precisa ser incluído no caso correspondente em outro sistema.
- Prioridade:
- Quando a prioridade de um caso em um sistema é atualizada, isso também ocorre com a prioridade do caso correspondente no outro sistema.
- Status do caso:
- Quando um caso é fechado em um sistema, ele também precisa ser fechado no outro.
Infraestrutura
Produtos do Google Cloud
O conector é um aplicativo do App Engine que usa o Cloud Firestore configurado no modo Datastore para armazenar dados sobre os casos sincronizados. Ele usa o Cloud Tasks para programar jobs com lógica automatizada de novas tentativas.
Para acessar casos no Customer Care, o conector usa uma conta de serviço para chamar a API Cloud Support V2. Você precisa conceder as permissões apropriadas à conta de serviço para autenticação.
Seu CRM
O conector acessa casos no seu CRM usando um mecanismo fornecido por você. Talvez você precise chamar uma API exposta pelo seu CRM.
Considerações de segurança para sua organização
O conector sincroniza casos na organização em que ele foi criado e em todos os projetos filhos da organização. Isso talvez permita que os usuários na organização acessem dados de suporte ao cliente que você não quer que eles acessem. Considere com atenção como você estrutura os papéis do IAM para manter a segurança na organização.
Design detalhado
Configurar CSAPI
Para configurar a CSAPI, siga estas etapas:
- Comprar um serviço de suporte do Cloud Customer Care para sua organização.
- No projeto em que você executará o conector, ative a API Cloud Support.
- Receba as credenciais da conta de serviço padrão do Apps Framework a ser usada pelo conector .
- Conceda os seguintes papéis à conta de serviço no nível da organização:
Tech Support Editor
Organization Viewer
Para mais informações sobre como configurar a CSAPI, consulte o Guia do usuário da API Cloud Support V2.
Chamar CSAPI
Usamos o Python para chamar a CSAPI. Recomendamos as duas maneiras de chamar a CSAPI com Python:
- Bibliotecas de cliente geradas a partir de protos. Eles são mais modernos e idiomáticos, mas não oferecem suporte à chamada de endpoints de anexo da CSAPI. Consulte Gerador de GAPIC para saber mais.
- Bibliotecas de cliente geradas a partir do documento de descoberta. Os arquivos são mais antigos, mas oferecem suporte para anexos. Consulte Cliente de API do Google para saber mais.
Veja um exemplo de como chamar a CSAPI usando bibliotecas de cliente criadas a partir do documento de descoberta:
"""
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()
Para mais exemplos de como chamar a CSAPI, consulte este repositório.
Nomes, IDs e números de recursos do Google
Permita que organization_id
seja o ID da sua organização. Você pode criar casos no Customer Care na sua organização ou em um projeto dela. Permita que project_id
seja o nome de um projeto em que você possa criar casos.
Nome do caso
O nome de um caso é semelhante ao seguinte:
organizations/{organization_id}/cases/{case_number}
projects/{project_id}/cases/{case_number}
Em que case_number
é o número atribuído ao caso. Por exemplo, 51234456
.
Nome do comentário
O nome de um comentário é semelhante ao seguinte:
organizations/{organization_id}/cases/{case_number}/comments/{comment_id}
Em que comment_id
é um número atribuído a um comentário. Por exemplo, 3
. Além das organizações, os projetos também podem ser usados por pais.
Nome do anexo
O nome de um anexo é semelhante ao seguinte:
organizations/{organization_id}/cases/{case_number}/attachments/{attachment_id}
Em que attachment_id
é o ID de um anexo em um caso, se houver. Por exemplo, 0684M00000JvBpnQAF
. Além das organizações, os projetos também podem ser usados por pais.
Entidades do Firestore
CaseMapping
Um CaseMapping
é um objeto definido para armazenar metadados sobre um caso.
Ele é criado para cada caso sincronizado e excluído quando não é mais necessário. Para saber mais sobre os tipos de dados aceitos no Firebase, consulte Tipos de dados compatíveis.
Um CaseMapping
tem as seguintes propriedades:
Propriedade | Descrição | Tipo | Exemplo |
---|---|---|---|
ID |
Chave primária. Atribuído automaticamente pelo Firestore quando o CaseMapping é criado. |
Inteiro | 123456789 |
googleCaseName |
O nome completo do caso, com o ID da organização ou do projeto e o número do caso. | String de texto | organizations/123/cases/456 |
companyCaseID |
O ID do caso no seu CRM. | Inteiro | 789 |
newContentAt |
A última vez que um novo conteúdo foi detectado no caso do Google ou no seu CRM. | Data e hora | 0001-01-01T00:00:00Z |
resolvedAt |
Um carimbo de data/hora de quando o caso do Google foi resolvido. Usado para excluir CaseMappings quando eles não forem mais necessários. |
Data e hora | 0001-01-01T00:00:00Z |
companyUpdatesSyncedAt |
Um carimbo de data/hora da última vez que o conector pesquisou no seu CRM e sincronizou atualizações com o caso do Google. Usado para detectar interrupções. | Data e hora | 0001-01-01T00:00:00Z |
googleUpdatesSyncedAt |
Um carimbo de data/hora da última vez que o conector pesquisou no Google e sincronizou atualizações com o caso do seu CRM. Usado para detectar interrupções. | Data e hora | 0001-01-01T00:00:00Z |
outageCommentSentToGoogle |
Caso uma falha temporária seja detectada, se um comentário foi adicionado ao caso do Google. Usado para evitar que vários comentários de falha temporária sejam adicionados. | Booleano | False |
outageCommentSentToCompany |
Caso uma falha temporária seja detectada, se um comentário foi adicionado ao caso do seu CRM. Usado para evitar que vários comentários de falha temporária sejam adicionados. | Booleano | False |
priority |
O nível de prioridade do caso. | Inteiro | 2 |
Global
Global
é um objeto que armazena variáveis globais para o conector.
Somente um objeto Global
é criado e nunca é excluído. Esta é a aparência dela:
Propriedade | Descrição | Tipo | Exemplo |
---|---|---|---|
ID | Chave primária. Atribuído automaticamente pelo Firestore quando este objeto é criado. | Inteiro | 123456789 |
google_last_polled_at |
Hora em que o atendimento ao cliente foi consultado pela última vez em busca de atualizações. | Data e hora | 0001-01-01T00:00:00Z |
company_last_polled_at |
A hora em que sua empresa foi pesquisada pela última vez para atualizações. | Data e hora | 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.