Puedes sincronizar tus casos de asistencia entre Google Cloud y tu sistema de administración de relaciones con clientes (CRM) (como Jira Service Desk, Zendesk y ServiceNow) creando un conector para integrarlos.
Este conector usa la API de Cloud Support (CSAPI) de Atención al cliente. Este documento proporciona un ejemplo de cómo puedes crear un conector y usarlo. Puedes modificar el diseño para que se adapte a tu caso de uso.
Supuestos
Realizamos algunas suposiciones importantes sobre el funcionamiento de su CRM y el idioma en el que escribe el conector. Si tu CRM tiene capacidades diferentes, aún puedes crear un conector que funcione a la perfección. Sin embargo, es posible que debas implementarlo de una manera diferente a la de esta guía.
Esta guía se basa en las siguientes suposiciones:
- Estás compilando el conector con Python y el microframework de Flask.
- Suponemos que usas Flask, ya que es un marco de trabajo sencillo en el que puedes compilar una app pequeña. También puedes usar otros lenguajes o frameworks, como Java.
- Deseas sincronizar los archivos adjuntos, los comentarios, la prioridad, los metadatos y el estado del caso. No es necesario sincronizar todos esos datos, a menos que lo desees. Por ejemplo, no sincronices los archivos adjuntos si no lo deseas.
- Tu CRM expone extremos capaces de leer y escribir los campos que quieres sincronizar. Si deseas sincronizar todos los campos como lo hacemos en esta guía, asegúrate de que los extremos de tu CRM admitan las siguientes operaciones:
Operación Equivalente de CSAPI Obtén casos mediante un ID estático inmutable. cases.get
Crea casos. cases.create
Cierra casos. cases.close
Actualiza la prioridad de un caso. cases.patch
Enumera los archivos adjuntos de un caso. cases.attachments.list
Descarga un archivo adjunto sobre un caso. media.download
Subir un archivo adjunto a un caso media.upload
Enumerar los comentarios de un caso cases.comments.list
Agregar un nuevo comentario en un caso cases.comments.create
Realiza búsquedas en los casos.* cases.search
*Debes poder filtrar por la hora de la última actualización. También debe existir alguna forma de determinar qué casos deben sincronizarse con el servicio de atención al cliente. Por ejemplo, si los casos de tu CRM pueden tener campos personalizados, puedes propagar un campo booleano personalizado con el nombre synchronizeWithGoogleCloudSupport
y filtrar en función de ese campo.
Diseño de alto nivel
El conector se compila completamente con productos de Google Cloud y tu CRM. Es una aplicación de App Engine que ejecuta Python en el microframework de Flask. Esta herramienta usa Cloud Tasks para consultar periódicamente la CSAPI y tu CRM en busca de casos nuevos y actualizaciones de los existentes, y sincroniza los cambios entre ellos. Algunos metadatos sobre los casos se almacenan en Firestore, pero se borran cuando ya no son necesarios.
En el siguiente diagrama, se muestra el diseño de alto nivel:
Objetivos del conector
El objetivo principal del conector es que cuando se cree en tu CRM un caso que quieras sincronizar, se cree un caso correspondiente en Atención al cliente, y todas las actualizaciones posteriores del caso se sincronicen entre ellas. Del mismo modo, cuando se crea un caso en Atención al cliente, se debe sincronizar con tu CRM.
Específicamente, se deben sincronizar los siguientes aspectos de los casos:
- Creación de casos:
- Cuando se crea un caso en un sistema, el conector debe crear un caso correspondiente en el otro.
- Si un sistema no está disponible, el caso debe crearse en él una vez que lo esté.
- Comentarios:
- Cuando se agrega un comentario a un caso en un sistema, debe agregarse al caso correspondiente en el otro sistema.
- Archivos adjuntos:
- Cuando se agrega un archivo adjunto a un caso en un sistema, debe agregarse al caso correspondiente en el otro sistema.
- Prioridad:
- Cuando se actualiza la prioridad de un caso en un sistema, se debe actualizar la prioridad del caso correspondiente en el otro sistema.
- Estado del caso:
- Cuando un caso se cierra en un sistema, también debe cerrarse en el otro.
Infraestructura
Productos de Google Cloud
El conector es una aplicación de App Engine que usa Cloud Firestore configurado en modo Datastore para almacenar datos sobre los casos sincronizados. Usa Cloud Tasks para programar trabajos con una lógica de reintento automatizada.
Para acceder a los casos en Atención al cliente, el conector usa una cuenta de servicio para llamar a la versión 2 de la API de Cloud Support. Debes otorgar los permisos adecuados a la cuenta de servicio para la autenticación.
Tu CRM
El conector accede a los casos en tu CRM mediante un mecanismo proporcionado por ti. Es probable que llame a una API que expone tu CRM.
Consideraciones de seguridad para tu organización
El conector sincroniza casos en la organización en la que lo compilas y todos los proyectos secundarios de la organización. Esto podría permitir que los usuarios de esa organización accedan a datos de asistencia al cliente a los que quizás no quieras que accedan. Analiza con atención cómo estructuras tus roles de IAM para mantener la seguridad en tu organización.
Diseño detallado
Configura la CSAPI
Para configurar la CSAPI, sigue estos pasos:
- Compra un servicio de asistencia al cliente de Cloud para tu organización.
- Para el proyecto en el que ejecutarás el conector, habilita la API de Cloud Support.
- Obtener las credenciales de la cuenta de servicio predeterminada de Apps Framework que usará el conector
- Otorga los siguientes roles a la cuenta de servicio a nivel de la organización:
Tech Support Editor
Organization Viewer
Para obtener más información sobre la configuración de la CSAPI, consulta la guía del usuario de la API de Cloud Support V2.
Llamar a CSAPI
Usamos Python para llamar a la CSAPI. Recomendamos las siguientes dos formas de llamar a la CSAPI con Python:
- Bibliotecas cliente generadas a partir de sus protos. Son más idiomáticos y modernos, pero no admiten llamar a los extremos de archivos adjuntos de CSAPI. Consulta Generador de GAPIC para obtener más información.
- Bibliotecas cliente generadas a partir de su documento de descubrimiento. Son más antiguas, pero admiten archivos adjuntos. Consulta Cliente de la API de Google para obtener más información.
A continuación, se muestra un ejemplo de cómo llamar a la CSAPI con bibliotecas cliente realizadas a partir de su documento de descubrimiento:
"""
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 ver más ejemplos de llamadas a CSAPI, consulta este repositorio.
Nombres, IDs y números de recursos de Google
Deja que organization_id
sea el ID de tu organización. Puedes crear casos en la Atención al cliente de tu organización o en un proyecto de la organización. Deja que project_id
sea el nombre de un proyecto en el que puedes crear casos.
Nombre del caso
El nombre de un caso se ve de la siguiente manera:
organizations/{organization_id}/cases/{case_number}
projects/{project_id}/cases/{case_number}
Donde case_number
es el número asignado al caso. Por ejemplo, 51234456
Nombre del comentario
El nombre de un comentario tiene el siguiente aspecto:
organizations/{organization_id}/cases/{case_number}/comments/{comment_id}
Donde comment_id
es un número que se asigna a un comentario. Por ejemplo, 3
Además, además de las organizaciones, también se permiten los proyectos con madres o padres.
Nombre del archivo adjunto
El nombre de un archivo adjunto tiene el siguiente aspecto:
organizations/{organization_id}/cases/{case_number}/attachments/{attachment_id}
Donde attachment_id
es el ID de un adjunto en un caso, si corresponde. Por ejemplo, 0684M00000JvBpnQAF
Además, además de las organizaciones, también se permiten los proyectos con madres o padres.
Entidades de Firestore
CaseMapping
Un CaseMapping
es un objeto definido para almacenar metadatos sobre un caso.
Se crea para cada caso que se sincroniza y se borra cuando ya no es necesario. Para obtener más información sobre los tipos de datos admitidos en Firebase, consulta Tipos de datos admitidos.
Un elemento CaseMapping
tiene las siguientes propiedades:
Propiedad | Descripción | Tipo | Ejemplo |
---|---|---|---|
ID |
Clave primaria. Firestore lo asigna automáticamente cuando se crea el CaseMapping . |
Integer | 123456789 |
googleCaseName |
El nombre completo del caso, con el ID de la organización o del proyecto y el número de caso. | String de texto | organizations/123/cases/456 |
companyCaseID |
El ID del caso en tu CRM. | Integer | 789 |
newContentAt |
La última vez que se detectó contenido nuevo en el caso de Google o en el caso de tu sistema de CRM. | Fecha y hora | 0001-01-01T00:00:00Z |
resolvedAt |
Una marca de tiempo del momento en que se resolvió el caso de Google Se usa para borrar CaseMappings cuando ya no son necesarios. |
Fecha y hora | 0001-01-01T00:00:00Z |
companyUpdatesSyncedAt |
Una marca de tiempo de la última vez que el conector sondeó correctamente tu CRM en busca de actualizaciones y sincronizó las actualizaciones con el caso de Google. Se usa para detectar interrupciones. | Fecha y hora | 0001-01-01T00:00:00Z |
googleUpdatesSyncedAt |
Una marca de tiempo de la última vez que el conector realizó correctamente una consulta a Google en busca de actualizaciones y sincronizó las actualizaciones con el caso de tu sistema de CRM. Se usa para detectar interrupciones. | Fecha y hora | 0001-01-01T00:00:00Z |
outageCommentSentToGoogle |
Indicar si se agregó un comentario al caso de Google en caso de que se detecte una interrupción. Se usa para evitar que se agreguen varios comentarios sobre interrupciones. | Booleano | False |
outageCommentSentToCompany |
Indica si se agregó un comentario al caso de tu CRM en caso de que se detecte una interrupción. Se usa para evitar que se agreguen varios comentarios sobre interrupciones. | Booleano | False |
priority |
El nivel de prioridad del caso. | Integer | 2 |
Global
Global
es un objeto que almacena variables globales para el conector.
Solo se crea un objeto Global
y nunca se borra. Se ve del siguiente modo:
Propiedad | Descripción | Tipo | Ejemplo |
---|---|---|---|
ID | Clave primaria. Firestore lo asigna automáticamente cuando se crea este objeto. | Integer | 123456789 |
google_last_polled_at |
Indica la hora en que se realizó la última consulta al equipo de Atención al cliente para comprobar si hay actualizaciones. | Fecha y hora | 0001-01-01T00:00:00Z |
company_last_polled_at |
La fecha y hora en que tu empresa se sometió a la última encuesta para obtener actualizaciones. | Fecha y 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.