Hola Habr! Esta es la primera publicación en nuestro blog. Muchas personas nos conocen como un chat para el sitio, fue con él que comenzamos, y ahora ocupamos posiciones de liderazgo en el campo de los mensajeros comerciales. Poco a poco evolucionamos hacia una solución comercial integral que brinda muchas oportunidades para los clientes: devolución de llamada, comunicación con clientes a través de mensajería instantánea, redes sociales, aplicaciones móviles, PBX virtual, funciones CRM y mucho más.
Durante varios años, resolvimos con éxito muchos problemas técnicos, acumulamos muchas cosas interesantes y, en algunos lugares, la experiencia única, por supuesto, escribió nuestras muletas y bicicletas. Con esta publicación, comenzamos una serie de artículos en los que compartiremos nuestra experiencia en el desarrollo, la construcción de procesos en un equipo completamente remoto, hablaremos sobre nuestra arquitectura, soluciones técnicas que nos permiten servir de manera efectiva a cientos de miles de clientes en todo el mundo.
Jivosite hoy es:- 250 mil clientes en todo el mundo;
- 150 millones de impresiones de widgets por día;
- 3,5 millones de mensajes por día;
- 10 millones de chats por mes;
- 1M de conexiones simultáneas;
- Más de 250 servidores en producción.
Como la mayoría de las personas nos conocen como chat para un sitio, probablemente comencemos con él. En este artículo, le mostraremos cómo conectar su código a un sitio de terceros y a qué debe prestar atención como ejemplo de nuestros muchos años de experiencia trabajando con el chat. Este artículo será útil para aquellos que planean o ya están desarrollando un servicio de complemento, y simplemente para todos los que estén interesados en este tema.
Punto de entrada
El teatro comienza con una percha y el servicio conectado con un código de inserción. Es el punto de entrada para cualquier servicio o módulo en el sitio. Como regla general, se puede encontrar en las instrucciones de instalación, después de lo cual es necesario agregarlo al código HTML del sitio, y luego hay "magia", que carga e inicializa el script de cierta manera.
<script src="https://site.com/file.js"></script>
Parece que podría ser más fácil conectar el script al sitio?
De manera estándar, solo necesita agregar una etiqueta de script al código HTML de la página. Pero, de hecho, esta es una etapa importante, que esconde muchas trampas. Por ejemplo, identificación del usuario, implementación de un canal de carga de script de respaldo, personalización de apariencia o lógica, velocidad de carga de la página, etc. Pero hablemos de todo en orden.
Identificación
Solo porque no es muy interesante para nadie conectar un script, seguro que el script realiza algún tipo de lógica, y esta lógica está vinculada al usuario. Por ejemplo, la ID del contador, APP_ID de la red social, en nuestro caso, esta es la ID del canal de comunicación creado. Es decir, el script debe identificar al usuario en las solicitudes al servidor. Para identificar al cliente a través del código de inserción, hay tres opciones de implementación.
Opcion # 1 <script async src="https://site.com/file.js?id=123"></script>
Pase la identificación directamente en el enlace al archivo y del lado del servidor de alguna manera, inclúyalo en el script. En este caso, el servidor tendrá que escribir la ID en el archivo sobre la marcha o formar una cadena JS con la ID que cargará file.js. Esta lógica es similar a la implementación de solicitudes JSONP.

Durante mucho tiempo trabajamos en este principio, pero las desventajas de este enfoque son que se agrega la carga "inactiva" en el servidor y la necesidad de implementar el almacenamiento en caché del servidor.
Opcion # 2 <script src="https://site.com/file.js" [async]></script> <script type=”text/javascript”> window.serviceNameId = “123”; </script>
Atributo asíncrono: le dice al navegador que no hay necesidad de esperar a que se cargue el script para construir el DOM, el script debe ejecutarse inmediatamente después de la carga. Esto reduce el tiempo de carga de la página, pero también hay un reverso de la moneda: el script se puede ejecutar antes de que el DOM esté listo para funcionar.Una de las implementaciones más populares, incluidos los grandes servicios, hace exactamente eso, solo la sintaxis es diferente, pero la esencia de todo es la misma.

Este enfoque tiene dos desventajas principales, la primera, el código de inserción es complicado, y la segunda, el orden de ejecución de este código es muy importante, de lo contrario, nada funcionará. Además, debe elegir entre velocidad (asíncrona) y estabilidad (sin asíncrona), la mayoría elige la segunda opción.
Opción # 3 <script async src="https://site.com/file.js?id=123"></script>
De manera similar a la primera opción, transfiera la ID en el enlace al archivo, pero recupérela en el navegador y no en el servidor. No es tan simple como parece, pero es posible. La API del navegador tiene una propiedad document.currentScript, devuelve un enlace a un script que está cargado y actualmente se está ejecutando en el navegador. Sabiendo esto, puede calcular la ID, para esto necesita obtener la propiedad document.currentScript.src y extraer regularmente la ID de ella.

Hay una cosa pero: document.currentScript no es compatible con todos los navegadores. Para los navegadores que no admiten esta propiedad, se nos ocurrió un truco interesante. En el código file.js, puede lanzar una excepción "falsa" especial envuelta en try / catch, después de lo cual se lanzará la URL del script en el que se produjo el error en la pila de errores. La URL contendrá la ID que obtenemos con la misma regularidad.
Se obtiene este tipo de magia, pero funciona. No hay problemas con el orden de ejecución, el código de inserción parece simple y no hay sobrecarga en el servidor. En los últimos dos años, hemos estado utilizando tal enfoque, aunque el código de inserción en sí es diferente, pero el principio es el mismo.
Configuraciones
En la mayoría de los casos, los scripts de complementos tienen configuraciones que son responsables de la apariencia o la lógica del trabajo. Esta configuración debe "incluirse" en el script del complemento, para esto hay dos enfoques fundamentalmente diferentes.
Enfoque n. ° 1 <script async src="https://site.com/file.js"></script> <script type=”text/javascript”> window.serviceName = {color: “red”, title: “”, ...}; </script>
Este enfoque también incluye pasar la configuración en los parámetros GET a la URL del script, similar a la opción # 1 de la sección "Identificación". El enfoque es que si el cliente desea cambiar la configuración, debe editar el código de inserción y actualizarlo en el sitio.

Esto es bueno porque todas las configuraciones se almacenan en el cliente y no es necesario que se almacenen en el servidor, desarrolle y mantenga toda la lógica empresarial relacionada con esto. La principal desventaja de este enfoque es el inconveniente para el cliente, tiene que hacer todo manualmente, y si hay muchas configuraciones, el código de inserción se convierte en una hoja difícil de mantener, en la que es fácil cometer un error. Y para que las actualizaciones surtan efecto, debe actualizar el sitio, estos son los gestos adicionales de los desarrolladores y administradores.
Enfoque n. ° 2 <script async src="https://site.com/file.js?id=123"></script>
El segundo enfoque es que si es necesario cambiar la configuración, el cliente no necesita modificar el código de inserción, todas las configuraciones se almacenan en el servidor. Para cambiar la configuración, vaya al panel gráfico, cambie los parámetros necesarios y haga clic en el botón "Guardar". Después de eso, la configuración se aplicará automáticamente a su sitio.

No es necesario comprender el código y hacer un despliegue para esto, esto puede hacerlo una persona que esté lejos de JavaScript, por ejemplo, un administrador. Por supuesto, esta opción es mucho más conveniente y simple para los usuarios, por eso la usamos. Pero debe pagar por conveniencia, este enfoque requiere el desarrollo y el soporte de la lógica en el servidor e implica una carga adicional en él. En los siguientes artículos, definitivamente le diremos cómo procesamos 150 millones de solicitudes diarias.
Compatibilidad con versiones anteriores
Es muy importante llegar a una versión madura del código de inserción lo más rápido posible. Porque actualizar los códigos de inserción ya instalados será extremadamente difícil. Un ejemplo de nuestra práctica: en las primeras versiones utilizamos ID numéricos, pero por razones de seguridad los reemplazamos por alfanuméricos. Resultó que es muy difícil lograr un cambio en el código de inserción ya instalado. Muchas personas ni siquiera saben qué es HTML y cómo están diseñados los sitios web. Por ejemplo, un sitio web fue creado por freelancers, se creó un estudio o un sitio web a través de un CMS / constructor, etc. En la mayoría de los casos, nuestros clientes solo trabajan con el panel de configuración de widgets. Desde entonces, todavía tenemos en nginx un mapa de reescritura de identificaciones antiguas a nuevas, que tiene alrededor de 40 mil registros.
.... /script/widget/config/15**90 /script/widget/config/bqZB**rjW5; /script/widget/config/15**94 /script/widget/config/qtfx**xnTi; /script/widget/config/15**95 /script/widget/config/fqmpa**4YX; /script/widget/config/15**97 /script/widget/config/Vr21g**nuT; /script/widget/config/15**98 /script/widget/config/8NXL5**F8E; /script/widget/config/15**00 /script/widget/config/Th2HN**6RJ; ....
Debido a esta característica, nos vemos obligados a mantener la compatibilidad con versiones anteriores del código de inserción para todas las refactorizaciones, de las cuales había aproximadamente 5 en nuestra memoria.
Aislamiento de código
Dado que el script está conectado a un sitio de terceros que ya tiene código JavaScript y CSS para el sitio y otros servicios, el objetivo principal es no dañar el sitio para que nuestro código no cambie la lógica, y mucho menos lo rompa. Esto podría ser un error de JavaScript que detiene el flujo de ejecución o estilos que anulan los estilos del sitio. Pero el código del sitio también puede afectar el script conectado, por ejemplo, se usa una biblioteca que modifica la API del navegador, después de lo cual el código deja de funcionar o no funciona como esperamos.
<script type="text/javascript"> </script>
<style type="text/css"> // body * { padding: 20px; } form input { display: block; border: 2px solid red; } </style>
Hay diferentes opciones para aislar el código. Por ejemplo, puede usar prefijos en variables JS, cierres, para no obstruir el contexto global, use algo como BEM para los estilos. Pero la forma más fácil es ejecutar el código en un iframe, resuelve la mayoría de los problemas de aislamiento, pero impone ciertas restricciones. Utilizamos una versión híbrida, en los siguientes artículos le informaremos más sobre el aislamiento del código.
Bloquear sitio de carga

Evento de carga: ocurre después de que la página web está completamente cargada, incluidas imágenes, estilos y scripts externos. Una característica importante es que en la mayoría de los sitios, la lógica JS, los scripts de terceros y los anuncios comienzan a funcionar cuando se produce este evento. Un punto muy importante para todos los scripts conectados es evitar un impacto negativo en este evento.
Esto sucede en los casos en que el servidor desde el que se carga el script responde durante un tiempo prolongado o no responde en absoluto: entonces el evento de carga se retrasa y la carga adicional de la página se bloquea esencialmente. En el caso de que el servidor no esté disponible, el evento de carga ocurrirá solo después de que la solicitud haya expirado, que es más de 60 s. Por lo tanto, los problemas en el servidor de carga de scripts esencialmente "rompen" los sitios, lo cual es inaceptable.
Experiencia personalEn el pasado, trabajaba para una empresa que tenía un sitio web con citas simultáneas en línea de 100K. En aquellos días, los botones "Compartir en redes sociales" eran populares. Para que aparecieran en el sitio, tenía que conectar un script (sdk) desde las redes sociales deseadas. Un día, los colegas corrieron hacia nosotros y dijeron que nuestro sitio no funcionaba. Observamos el monitoreo, en el que todo era normal, y al principio no entendíamos cuál era el problema. Cuando comenzaron a cavar más profundo, se dieron cuenta de que los servidores cdn de Twitter estaban acostados, y su SDK no podía cargarse, esto nos impidió cargar el sitio durante aproximadamente 1,5 minutos. Es decir, después de abrir el sitio, se cargó un poco de HTML (el resto de SPA) y solo después de 1.5 minutos se cargó todo, el tiempo de espera de la solicitud funcionó. Tuvimos que organizar urgentemente un hotfix y eliminar su script del sitio. Después de repetir esta situación, decidimos eliminar el bloqueo Compartir.
En las primeras versiones del código de inserción, no tomamos esto en cuenta, y en caso de problemas técnicos de nuestra parte, para decirlo suavemente, incomodamos a nuestros clientes, pero con el tiempo lo arreglamos.
Solución <script type='text/javascript'> (function(){ var initCode = function () { </script>
La solución es simple: debe suscribirse al evento de una carga completa del sitio y solo luego cargar el script, para esto debe usar el código de inserción y no la etiqueta del script.
Velocidad de página de Google
Análisis de la versión móvil de habr.comLa mayoría de ellos prestan atención a la velocidad de carga del sitio, según muchos estudios, esto afecta directamente la ganancia, además, los algoritmos de búsqueda cuando la clasificación comenzó a tener en cuenta el tiempo de carga de la página. En este sentido, los propietarios de sitios a menudo usan herramientas similares para evaluar el rendimiento del sitio. Por lo tanto, es muy importante conectar de manera óptima el código al sitio, ya que afecta directamente su tiempo de descarga.
Esto significa que debe utilizar técnicas modernas para optimizar la carga de la página. Por ejemplo, use Gzip, guarde en caché los archivos estáticos y las solicitudes, use la carga asíncrona de scripts, comprima la estática con algoritmos modernos como WebP / Brotli / etc. y use otras optimizaciones. Regularmente realizamos auditorías y respondemos a advertencias y recomendaciones para cumplir con los requisitos actuales.
CDN
En las primeras versiones, descargamos estadísticas de los servidores de aplicaciones. Pero este enfoque tiene desventajas: tráfico costoso, lejanía de los visitantes del sitio y carga excesiva en el canal del servidor. Puede obstruir fácilmente el canal de los servidores de aplicaciones con el efecto habr de los sitios, ya que el tráfico estático es muy "pesado".
Para ahorrar presupuesto, estabilidad y reducir la latencia de la red, es óptimo descargar estadísticas de servidores especialmente diseñados para esto. Puede usar proveedores de CDN ya preparados, pero a gran escala no es barato y tiene que estar limitado por las capacidades que proporciona este o aquel proveedor.

Lo implementamos simplemente, pedimos servidores económicos en Rusia, Europa y América con tráfico ilimitado y un amplio canal. Es barato, no nos impone restricciones, podemos personalizar todo para nosotros y el mecanismo que funciona en el navegador garantiza la tolerancia a fallos. Actualmente, se cargan diariamente 1 TB de estadísticas de nuestros servidores CDN.
Tolerancia a fallos
Desafortunadamente, el mundo no es perfecto, se producen incendios, los enlaces ascendentes caen, los CD se sumergen por completo, el ILV bloquea las subredes y las personas cometen errores. Sin embargo, es necesario poder manejar tales situaciones y continuar trabajando.
MonitoreoPrimero debes entender que algo salió mal. Por supuesto, puede esperar hasta que los usuarios vengan y se quejen, pero es mejor configurar el monitoreo y las alertas, y después de los lanzamientos, verificar si todo está en orden. Monitoreamos muchos parámetros diferentes, tanto del servidor como del cliente, y si algo salió mal, lo vemos de inmediato. Por ejemplo, la cantidad de descargas de widgets o un aumento anormal del tráfico en los servidores CDN ha disminuido.
El número total de descargas de widgets para cada versiónColección de erroresJavaScript es un lenguaje muy específico y es fácil cometer un error. Además, el zoológico de navegadores en la web moderna es muy grande; lo que funciona en el último Chrome no es un hecho que funcione en Safari o Firefox. Por lo tanto, es muy importante configurar la recopilación de errores desde el navegador y responder a los picos a tiempo. Si su código funciona en un iframe, puede hacerlo mediante el seguimiento del controlador global window.onerror y, en caso de error, enviar datos al servidor. Si el código funciona fuera del iframe, entonces es muy difícil implementar la recopilación de errores.
El número total de errores de todos los sitios y navegadores.
Información de error específicoConmutación por error de CDNYa escribí anteriormente que todo tiene la propiedad de caerse, por lo que es importante manejar estas situaciones y mejor, automáticamente. Pasamos por varias etapas del retroceso de los servidores CDN, comenzamos desde el manual y finalmente encontramos una manera de hacerlo de manera automática y óptima para el navegador.
En modo manual, esto funcionó simplemente: SMS recibió administradores que decían que el CDN estaba inactivo, realizaron ciertas manipulaciones, después de lo cual el widget comenzó a cargarse desde los servidores de aplicaciones. Esto puede llevar de 5 minutos a 2 horas.
Para implementar la recuperación automática, debe detectar de alguna manera que el script ha comenzado a cargarse, pero esto no es tan fácil como parece. El navegador no proporciona la capacidad de monitorear estados intermedios de la carga de la etiqueta del script, como el evento onprogress en XMLHttpRequest, pero solo informa el evento cuando el script se carga y ejecuta. También es imposible que un tiempo aceptable descubra que el servidor no está disponible actualmente, el único evento de error se activa después de que expira el tiempo de espera de la solicitud, más de 1 minuto. En un minuto, el visitante ya puede abandonar la página, pero el script no se cargará.
Probamos diferentes opciones, simples y complejas, pero al final se nos ocurrió una solicitud de ping para un servidor CDN. Funciona así: primero hacemos ping al servidor CDN, si responde, luego cargamos el widget desde él. Para implementar este esquema de manera óptima para el navegador y nuestros servidores, utilizamos una solicitud HEAD ligera (sin cuerpo), y durante las descargas posteriores no lo hacemos hasta que se actualiza la versión del widget, porque el widget ya está en la memoria caché del navegador.

Por lo tanto, recibimos una detección muy rápida y automática de la disponibilidad del servidor estático y, en caso de una caída, cambiamos al servidor de respaldo casi sin demora.
Cargador
Para cargar su script en un sitio de terceros, debe tener en cuenta muchos puntos, pero es difícil implementar esta lógica en el código de inserción, ya que simplemente se convertirá en "carne". Pero aún necesita hacer esto, para esto hemos creado un pequeño módulo que gestiona toda esta lógica "bajo el capó" y carga el código principal del widget. Primero se carga e implementa CDN Failover, almacenamiento en caché, compatibilidad con códigos de inserción antiguos, pruebas A / B, diseño gradual de la nueva versión del widget y muchas otras funciones.

Por lo tanto, en etapas, llegamos a un esquema que cubre los casos principales de carga e inicialización del widget. Ella ha demostrado su efectividad a lo largo de los años de uso en una gran cantidad de sitios diferentes. Al mismo tiempo, el código de inserción sigue siendo simple y universal, ya que no tiene lógica y podemos cambiarlo en cualquier momento, sin obligar a los usuarios a cambiar el código de inserción.
Servicios de terceros
Y, por último, vale la pena mencionar los servicios de terceros que se conectan al sitio o de alguna manera interactúan con los sitios: robots de búsqueda, análisis, varios analizadores, etc. Estos servicios dejan una huella en el trabajo, tampoco debe olvidarse de esto. Te contaré algunos casos de nuestra práctica.
GooglebotLa aplicación de nuestro operador tiene la función "Visitantes", en la que puede ver los visitantes que actualmente están viendo el sitio, y diversa información sobre ellos: tiempo en el sitio, página, número de páginas visitadas, etc. En algún momento, los clientes comenzaron a quejarse de que estaban "colgando" visitantes de otros sitios, es decir, en el sitio que vendía iPhones, un cliente que supuestamente tenía una página llamada "Comprar crema para la cara". Cuando comenzaron a resolverlo, resultó que era GoogleBot, que, al cambiar de un sitio a otro, primero almacenó en caché LocalStorage y luego transfirió datos incorrectos al servidor.
La solución es simple, el servidor comenzó a ignorar los datos de GoogleBot.
Yandex.MetricaHay una característica maravillosa en la métrica: un navegador web que le permite ver lo que el usuario vio e hizo en forma de screencast. Para hacer esto, la métrica registra todas las acciones del usuario, y después de que un robot de métrica especial recorre los sitios, realiza las mismas acciones y lo registra. El problema era que para emular el navegador móvil del usuario, de acuerdo con nuestros datos, Firefox estaba activado en el modo de emulación móvil, pero el Agente de usuario en el bot era de escritorio.
Esto condujo al hecho de que al ver sesiones de usuarios móviles en el navegador web, la versión de escritorio del widget se abría en las grabaciones, aunque, de hecho, los usuarios abrían la versión móvil. Nuestros clientes pensaron que sí, y nos bombardearon con quejas. , , , , .
, , , .
, . , . , , NodeJS , 270 - , .
, !