Bonjour, Habr! Ceci est le premier article de notre blog. Beaucoup de gens nous connaissent comme un chat pour le site, c'est avec lui que nous avons commencé, et maintenant nous occupons des positions de leader dans le domaine des messagers d'affaires. Nous sommes progressivement devenus une solution commerciale complète qui offre de nombreuses opportunités aux clients: rappel, communication avec les clients via des messageries instantanées, réseaux sociaux, applications mobiles, PBX virtuel, fonctions CRM et bien plus encore.
Pendant plusieurs années, nous avons résolu avec succès de nombreux problèmes techniques, accumulé beaucoup de choses intéressantes et, à certains endroits, une expérience unique, bien sûr, a écrit nos béquilles et nos vélos. Avec cet article, nous commençons une série d'articles dans lesquels nous partagerons notre expérience dans le développement, la construction de processus dans une équipe complètement distante, parler de notre architecture, des solutions techniques qui nous permettent de servir efficacement des centaines de milliers de clients à travers le monde.
Jivosite est aujourd'hui:- 250 000 clients dans le monde;
- 150 millions d'impressions de widgets par jour;
- 3,5 millions de messages par jour;
- 10 millions de chats par mois;
- 1M de connexions simultanées;
- 250+ serveurs en production.
Étant donné que la plupart des gens nous connaissent comme un chat pour un site, nous allons probablement commencer par celui-ci. Dans cet article, nous allons montrer comment connecter votre code à un site tiers et à quoi vous devez prêter attention en tant qu'exemple de nos nombreuses années d'expérience dans le chat. Cet article sera utile à ceux qui envisagent ou développent déjà un service de plug-in, et simplement à tous ceux qui s'intéressent à ce sujet.
Point d'entrée
Le théâtre commence par un cintre, et le service connecté avec un code d'insertion. C'est le point d'entrée de tout service ou module sur le site. En règle générale, il peut être trouvé dans les instructions d'installation, après quoi il est nécessaire de l'ajouter au code HTML du site, puis il y a la «magie», qui charge et initialise le script d'une certaine manière.
<script src="https://site.com/file.js"></script>
Il semblerait qu'il serait plus facile de connecter le script au site?
Par défaut, il vous suffit d'ajouter une balise de script au code HTML de la page. Mais en fait, c'est une étape importante, qui cache de nombreux pièges. Par exemple, identification de l'utilisateur, implémentation d'un canal de chargement de script de sauvegarde, personnalisation de l'apparence ou de la logique, vitesse de chargement des pages, etc. Mais parlons de tout dans l'ordre.
Identification
Tout simplement parce qu'il n'est pas très intéressant pour quiconque de connecter un script, c'est sûr que le script exécute une sorte de logique, et cette logique est liée à l'utilisateur. Par exemple, l'ID du compteur, APP_ID du réseau social, dans notre cas, c'est l'ID du canal de communication créé. Autrement dit, le script doit identifier l'utilisateur dans les demandes adressées au serveur. Pour identifier le client via le code d'insertion, il existe trois options d'implémentation.
Option # 1 <script async src="https://site.com/file.js?id=123"></script>
Passez l'ID directement dans le lien vers le fichier et côté serveur, d'une certaine manière, jetez-le dans le script. Dans ce cas, le serveur devra écrire l'ID dans le fichier à la volée ou former une chaîne JS avec l'ID qui chargera file.js. Cette logique est similaire à l'implémentation des requêtes JSONP.

Pendant longtemps, nous avons travaillé sur ce principe, mais les inconvénients de cette approche sont que la charge «inactive» sur le serveur et la nécessité d'implémenter la mise en cache du serveur sont ajoutées.
Option n ° 2 <script src="https://site.com/file.js" [async]></script> <script type=”text/javascript”> window.serviceNameId = “123”; </script>
Attribut Async - indique au navigateur qu'il n'est pas nécessaire d'attendre le chargement du script pour construire le DOM, le script doit être exécuté immédiatement après le chargement. Cela réduit le temps de chargement des pages, mais il y a aussi un revers à la médaille: le script peut être exécuté avant que le DOM ne soit prêt à fonctionner.L'une des implémentations les plus populaires, y compris les grands services, fait exactement cela, seule la syntaxe est différente, mais l'essence de tous est la même.

Cette approche présente deux inconvénients principaux, le premier - le code d'intégration est compliqué, et le second - l'ordre d'exécution de ce code est très important, sinon rien ne fonctionnera. De plus, vous devez faire un choix entre la vitesse (async) et la stabilité (sans async), la plupart choisissent la 2e option.
Option n ° 3 <script async src="https://site.com/file.js?id=123"></script>
De la même manière que pour la première option, transférez l'ID dans le lien vers le fichier, mais récupérez-le dans le navigateur et non sur le serveur. Ce n'est pas aussi simple qu'il y paraît, mais c'est possible. L'API du navigateur possède une propriété document.currentScript, elle renvoie un lien vers un script chargé et en cours d'exécution dans le navigateur. Sachant cela, vous pouvez calculer l'ID, pour cela, vous devez obtenir la propriété document.currentScript.src et en extraire régulièrement l'ID.

Il y a une chose mais: document.currentScript n'est pas pris en charge par tous les navigateurs. Pour les navigateurs qui ne prennent pas en charge cette propriété, nous avons trouvé un hack intéressant. Dans le code file.js, vous pouvez lever une exception "fake" spéciale enveloppée dans try / catch, après quoi l'URL du script dans lequel l'erreur s'est produite dans la pile d'erreurs sera levée. L'URL contiendra l'ID que nous obtenons avec la même régularité.
Ce type de magie est obtenu, mais cela fonctionne. Il n'y a aucun problème avec l'ordre d'exécution, le code d'insertion semble simple et il n'y a pas de surcharge sur le serveur. Au cours des deux dernières années, nous avons utilisé une telle approche, bien que le code d'insertion lui-même soit différent, mais le principe est le même.
Paramètres
Dans la plupart des cas, les scripts de plug-in ont des paramètres qui sont responsables de l'apparence ou de la logique du travail. Ces paramètres doivent être "jetés" dans le script du plug-in, pour cela il existe deux approches fondamentalement différentes.
Approche n ° 1 <script async src="https://site.com/file.js"></script> <script type=”text/javascript”> window.serviceName = {color: “red”, title: “”, ...}; </script>
Cette approche comprend également la transmission des paramètres des paramètres GET à l'URL du script, similaire à l'option n ° 1 de la section "Identification". L'approche est que si le client souhaite modifier les paramètres, il doit alors modifier le code d'intégration et le mettre à jour sur le site.

C'est bien car tous les paramètres sont stockés sur le client et ils n'ont pas besoin d'être stockés sur le serveur, développez et maintenez toute la logique métier liée à cela. Le principal inconvénient de cette approche est l'inconvénient pour le client, il doit tout faire manuellement, et s'il y a beaucoup de paramètres, le code d'intégration se transforme en une feuille difficile à maintenir, dans laquelle il est facile de faire une erreur. Et pour que les mises à jour prennent effet, vous devez mettre à jour le site, ce sont les gestes supplémentaires des développeurs et des administrateurs.
Approche n ° 2 <script async src="https://site.com/file.js?id=123"></script>
La deuxième approche est que s'il est nécessaire de modifier les paramètres, le client n'a pas besoin de modifier le code d'insertion, tous les paramètres sont stockés sur le serveur. Pour modifier les paramètres, allez dans le panneau graphique, modifiez les paramètres nécessaires et cliquez sur le bouton "Enregistrer". Après cela, les paramètres seront automatiquement appliqués à son site!

Pas besoin de comprendre le code et de faire un déploiement pour cela, cela peut être fait par une personne qui est loin de JavaScript, par exemple, un manager. Bien sûr, cette option est beaucoup plus pratique et plus simple pour les utilisateurs, c'est pourquoi nous l'utilisons. Mais il faut payer pour la commodité, cette approche nécessite le développement et le support de la logique sur le serveur et implique une charge supplémentaire sur celui-ci. Dans les articles suivants, nous vous expliquerons certainement comment nous traitons quotidiennement 150 millions de ces demandes.
Compatibilité descendante
Il est très important d'obtenir une version mature du code d'intégration le plus rapidement possible. Parce que la mise à jour des codes d'insertion déjà installés sera extrêmement difficile. Un exemple de notre pratique: dans les premières versions, nous utilisions des identifiants numériques, mais pour des raisons de sécurité, nous les avons remplacés par des identifiants alphanumériques. Il s'est avéré qu'il est très difficile de modifier le code d'intégration déjà installé. Beaucoup de gens ne savent même pas ce qu'est le HTML et comment les sites Web sont conçus. Par exemple, un site Web a été créé par des pigistes, un studio ou un site Web a été créé via un CMS / constructeur, etc. Dans la plupart des cas, nos clients ne travaillent qu'avec le panneau des paramètres du widget. Depuis lors, nous avons toujours dans nginx une carte de réécriture des anciens ID vers les nouveaux, qui compte environ 40 000 enregistrements.
.... /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; ....
En raison de cette fonctionnalité, nous sommes obligés de maintenir la compatibilité descendante du code intégré pour tous les refactoring, dont il y avait environ 5 dans notre mémoire.
Isolement du code
Étant donné que le script est connecté à un site tiers qui dispose déjà de code JavaScript et CSS pour le site et d'autres services, l'objectif principal n'est pas de nuire au site afin que notre code ne change pas la logique, encore moins de le casser. Il peut s'agir d'une erreur JavaScript qui arrête le flux d'exécution ou de styles qui remplacent les styles de site. Mais le code du site peut également affecter le script connecté, par exemple, une bibliothèque est utilisée qui modifie l'API du navigateur, après quoi le code cesse de fonctionner ou ne fonctionne pas comme prévu.
<script type="text/javascript"> </script>
<style type="text/css"> // body * { padding: 20px; } form input { display: block; border: 2px solid red; } </style>
Il existe différentes options pour isoler le code. Par exemple, vous pouvez utiliser des préfixes dans les variables JS, les fermetures, afin de ne pas obstruer le contexte global, utilisez quelque chose comme BEM pour les styles. Mais le moyen le plus simple consiste à exécuter le code dans un iframe, il résout la plupart des problèmes d'isolement, mais impose certaines restrictions. Nous utilisons une version hybride, nous vous en dirons plus sur l'isolement du code dans les articles suivants.
Site de chargement de blocs

Événement de chargement - se produit après le chargement complet de la page Web, y compris les images, les styles et les scripts externes. Une caractéristique importante est que sur la plupart des sites, la logique JS, les scripts et les publicités tiers commencent à fonctionner sur l'occurrence de cet événement. Un point très important pour tous les scripts connectés est d'éviter un impact négatif sur cet événement.
Cela se produit dans les cas où le serveur à partir duquel le script est chargé répond pendant longtemps ou ne répond pas du tout: alors l'événement onload est retardé et le chargement de page supplémentaire est essentiellement bloqué. Dans le cas où le serveur n'est pas disponible, l'événement onload ne se produit qu'après expiration du délai de la demande, qui est supérieur à 60 s. Ainsi, les problèmes sur le serveur de téléchargement de scripts "cassent" essentiellement les sites, ce qui est inacceptable.
Expérience personnelleDans le passé, j'ai travaillé pour une entreprise qui avait un site Web avec des rencontres en ligne simultanées de 100K. À cette époque, les boutons «Partager sur les réseaux sociaux» étaient populaires. Pour qu'ils apparaissent sur le site, vous avez dû connecter un script (sdk) à partir des réseaux sociaux souhaités. Un jour, des collègues ont couru vers nous et nous ont dit que notre site ne fonctionnait pas! Nous avons examiné la surveillance, dans laquelle tout était normal, et au début, nous n'avons pas compris quel était le problème. Quand ils ont commencé à creuser plus profondément, ils ont réalisé que les serveurs cdn de Twitter étaient couchés et que leur SDK ne pouvait pas se charger, ce qui nous a empêché de charger le site pendant environ 1,5 minute. Autrement dit, après l'ouverture du site, un peu de HTML a été chargé (le reste du SPA) et seulement après 1,5 minute, tout a été chargé, le délai de requête même a fonctionné. Nous avons dû de toute urgence organiser un correctif et supprimer leur script du site. Après avoir répété cette situation, nous avons décidé de supprimer le bloc Partager.
Dans les premières versions du code insert, nous n'en avons pas tenu compte, et en cas de problèmes techniques de notre côté, pour le moins, nous gênons nos clients, mais au fil du temps nous l'avons corrigé.
Solution <script type='text/javascript'> (function(){ var initCode = function () { </script>
La solution est simple, vous devez vous abonner à l'événement d'une charge complète du site et ensuite seulement charger le script, pour cela vous devez utiliser le code d'intégration, pas la balise de script.
Google pagespeed
Analyse de la version mobile de habr.comLa plupart d'entre eux prêtent attention à la vitesse de chargement du site, selon de nombreuses études, cela affecte directement le profit, en outre, les algorithmes de recherche lorsque le classement a commencé à prendre en compte le temps de chargement de la page. À cet égard, les propriétaires de sites utilisent souvent des outils similaires pour évaluer les performances du site. Par conséquent, il est très important de connecter de manière optimale le code au site, car cela affecte directement son temps de téléchargement.
Cela signifie que vous devez utiliser des techniques modernes pour optimiser le chargement des pages. Par exemple, utilisez Gzip, mettez en cache les fichiers et les requêtes statiques, utilisez le chargement de script asynchrone, compressez la statique avec des algorithmes modernes tels que WebP / Brotli / etc et utilisez d'autres optimisations. Nous effectuons régulièrement des audits et répondons aux avertissements et recommandations pour répondre aux exigences actuelles.
Cdn
Dans les premières versions, nous avons téléchargé des statiques à partir de serveurs d'applications. Mais cette approche présente des inconvénients: trafic coûteux, éloignement des visiteurs du site et charge excessive sur le canal du serveur. Vous pouvez facilement obstruer le canal des serveurs d'applications avec l'effet habr des sites, car le trafic statique est très "lourd".
Afin d'économiser du budget, de la stabilité et de réduire la latence du réseau, il est optimal de charger la statique à partir de serveurs spécialement conçus à cet effet. Vous pouvez utiliser des fournisseurs CDN prêts à l'emploi, mais à grande échelle, ce n'est pas bon marché et vous devez être limité par les capacités offertes par tel ou tel fournisseur.

Nous l'avons implémenté simplement, commandé des serveurs bon marché en Russie, en Europe et en Amérique avec un trafic illimité et un large canal. C'est bon marché, il ne nous impose aucune restriction, nous pouvons tout personnaliser pour nous-mêmes et la tolérance aux pannes est assurée par le mécanisme qui fonctionne dans le navigateur. Actuellement, 1 To de statique est chargé quotidiennement à partir de nos serveurs CDN.
Tolérance aux pannes
Malheureusement, le monde n'est pas parfait, des incendies se produisent, des liaisons montantes tombent, les DC se mettent complètement sous l'eau, l'ILV bloque les sous-réseaux et les gens font des erreurs. Néanmoins, il est nécessaire de pouvoir gérer de telles situations et de continuer à travailler.
SuiviVous devez d'abord comprendre que quelque chose s'est mal passé. Vous pouvez, bien sûr, attendre que les utilisateurs viennent se plaindre, mais il est préférable de configurer la surveillance et les alertes, et après les versions, vérifiez si tout est en ordre. Nous surveillons de nombreux paramètres différents, à la fois sur le serveur et sur le client, et en cas de problème, nous le voyons immédiatement. Par exemple, le nombre de téléchargements de widgets ou une augmentation anormale du trafic sur les serveurs CDN a diminué.
Le nombre total de téléchargements de widgets pour chaque versionCollecte d'erreursJavaScript est un langage très spécifique et il est facile de se tromper. De plus, le zoo des navigateurs sur le Web moderne est très grand; ce qui fonctionne dans le dernier Chrome n'est pas un fait qui fonctionnera dans Safari ou Firefox. Par conséquent, il est très important de configurer la collecte des erreurs à partir du navigateur et de répondre aux pointes dans le temps. Si votre code fonctionne dans un iframe, vous pouvez le faire en suivant le gestionnaire global window.onerror et, en cas d'erreur, envoyez des données au serveur. Si le code fonctionne en dehors de l'iframe, il est très difficile d'implémenter la collecte des erreurs.
Le nombre total d'erreurs de tous les sites et navigateurs
Informations d'erreur spécifiquesBasculement du CDNJ'ai déjà écrit ci-dessus que tout a la propriété de tomber, il est donc important de gérer ces situations et mieux - automatiquement. Nous sommes passés par plusieurs étapes de secours des serveurs CDN, en commençant par le manuel, et nous avons finalement trouvé un moyen de le faire automatiquement et de manière optimale pour le navigateur.
En mode manuel, cela a fonctionné simplement: SMS a reçu des administrateurs disant que le CDN était en panne, ils ont effectué certaines manipulations, après quoi le widget a commencé à se charger à partir des serveurs d'applications. Cela peut prendre de 5 minutes à 2 heures.
Pour implémenter le repli automatique, vous devez en quelque sorte détecter que le script a commencé à se charger, mais ce n'est pas aussi simple qu'il y paraît. Le navigateur ne permet pas de surveiller les états intermédiaires du chargement des balises de script, tels que l'événement onprogress dans XMLHttpRequest, mais signale uniquement l'événement lorsque le script est chargé et exécuté. Il est également impossible pour un temps acceptable de découvrir que le serveur est actuellement indisponible, le seul événement onerror se déclenche après l'expiration du délai d'expiration de la demande, plus d'une minute. Dans une minute, le visiteur peut déjà quitter la page, mais le script ne se charge pas.
Nous avons essayé différentes options, simples et complexes, mais à la fin nous avons trouvé une requête ping pour un serveur CDN. Cela fonctionne comme ceci: nous envoyons d'abord une requête ping au serveur CDN, s'il répond, puis nous chargeons le widget à partir de celui-ci. Pour implémenter ce schéma de manière optimale pour le navigateur et nos serveurs, nous utilisons une requête HEAD légère (sans corps), et lors des téléchargements ultérieurs, nous ne le faisons que lorsque la version du widget est mise à jour, car le widget est déjà dans le cache du navigateur.

Ainsi, nous avons reçu une détection très rapide et automatique de la disponibilité du serveur statique et en cas de chute, nous passons au serveur de sauvegarde presque sans délai.
Chargeur
Pour télécharger votre script sur un site tiers, vous devez prendre en compte de nombreux points, mais il est difficile d'implémenter cette logique dans le code d'intégration, car il se transformera simplement en "viande". Mais vous devez encore le faire, pour cela nous avons créé un petit module qui gère toute cette logique "sous le capot" et charge le code principal du widget. Il se charge d'abord et implémente le basculement CDN, la mise en cache, la compatibilité descendante avec les anciens codes intégrés, les tests A / B, la mise en place progressive de la nouvelle version du widget et de nombreuses autres fonctions.

Ainsi, par étapes, nous sommes arrivés à un schéma qui couvre les principaux cas de chargement et d'initialisation du widget. Il a fait ses preuves au fil des années d'utilisation sur un grand nombre de sites différents. Dans le même temps, le code d'insertion reste simple et universel, car il ne contient aucune logique et nous pouvons le modifier à tout moment, sans forcer les utilisateurs à modifier le code d'insertion.
Services tiers
Et enfin, il convient de mentionner les services tiers qui se connectent au site ou interagissent d'une manière ou d'une autre avec des sites: robots de recherche, analyses, divers analyseurs, etc. Ces services laissent une empreinte au travail, ne l'oubliez pas non plus. Je vais vous raconter quelques cas de notre pratique.
GooglebotL'application de notre opérateur dispose de la fonction «Visiteurs», dans laquelle vous pouvez voir les visiteurs qui consultent actuellement le site, et diverses informations à leur sujet: temps sur le site, page, nombre de pages vues, etc. À un moment donné, les clients ont commencé à se plaindre qu'ils «suspendaient» les visiteurs d'autres sites, c'est-à -dire sur le site vendant des iPhones, un client qui aurait une page intitulée «Acheter une crème pour le visage». Quand ils ont commencé à comprendre, il s'est avéré que c'était GoogleBot, qui, lors du passage d'un site à un autre, a d'abord mis en cache LocalStorage puis transféré des données incorrectes vers le serveur.
La solution est simple, le serveur a commencé à ignorer les données de GoogleBot.
Yandex.MetricaIl y a une merveilleuse fonctionnalité dans la métrique - un navigateur Web, qui vous permet de voir ce que l'utilisateur a vu et fait sous la forme d'un screencast. Pour ce faire, la métrique enregistre toutes les actions de l'utilisateur et, après qu'un robot de métrique spécial parcourt les sites, effectue les mêmes actions et les enregistre. Le problème était que pour émuler le navigateur mobile de l'utilisateur, selon nos données, Firefox était activé en mode d'émulation mobile, mais l'agent userAgent dans le bot était de bureau.
Cela a conduit au fait que lors de la visualisation des sessions d'utilisateurs mobiles dans le navigateur Web, la version de bureau du widget s'est ouverte sur les enregistrements, bien qu'en fait, les utilisateurs aient ouvert la version mobile. Nos clients l'ont pensé et nous ont bombardés de plaintes. , , , , .
, , , .
, . , . , , NodeJS , 270 - , .
, !