Что делать с чужими долгами?

в 19:13, , рубрики: teamlead, technical debt, макконнелл, Совершенный код, Управление продуктом, управление проектами, управление разработкой, метки: ,

Один из аспектов профессии разработчика — посвящение профанов в особенности процесса разработки ПО.
С. Макконнелл, Совершенный код

Цель этой публикации — поделиться опытом работы над проектом со сложной историей и тяжёлым наследием. После ухода из очередного т.н. «стартапа», я решил что хочу попробовать новых ощущений: enterprise, legacy, etc. Для этого взялся за работу над корпоративным приложением для транснационального концерна. Разработка на тот момент шла уже третий год, приложение пережило несколько поколений разработчиков, но стабильного релиза так и не было.

Полагаю публикация будет полезной:

  • разработчикам принимающим аналогичное решение, чтобы взвесить за и против
  • менеджерам «непростых» проектов, чтобы лучше понять причины и следствия технических проблем
  • и, конечно, просто любопытствующим

Затрагиваемые в статье вопросы:

  • Низкая компетенция разработчиков, и что с этим можно поделать?
  • Какие аргументы убедительны в глазах заказчика для нефункциональных изменений в проекте?
  • Почему работа аналитиков и QA очень важна с точки зрения разработки в частности и для проекта в целом?

Input

Что на входе? На собеседовании первого этапа я был заинтригован следующими подробностями:

  • Используемая версия фреймворка за годы разработки успела устареть
  • Релиза стабильной версии продукта за два года не состоялось
  • Бизнес-сложность системы порой зашкаливает

На собеседовании второго этапа, с участием проектного менеджера и ведущего разработчика, я получил следующую порцию нюансов:

  • У текущей команды разработки низкий уровень компетенции в используемых фреймворке, языке, тестировании, IoC, объектно-ориентированном дизайне, шаблонах проектирования и архитектуре
  • Предметная область действительно непроста
  • Заказчик убеждён что проблем нет

К моменту принятия положительного решения об участии в проекте, я не видел ни одного хорошего или надёжного места в нём: абсолютно всё, что мне удалось узнать, являлось признаками проблем. Именно это и подтолкнуло меня к положительному ответу =)

Резюмируя положение дел на старте, можно выделить следующие балластные категории, которые представляли риск для успеха дальнейшей разработки:

  • Низкая компетенция разработчиков
  • Низкое качество кода (вытекает из предыдущего пункта), большой технический долг
  • Отсутствие налаженной инфраструктуры
  • Отсутствие автоматизированного тестирования
  • Отсутствие культуры приёмочного тестирования
  • Отсутствие культуры версионирования, внесения изменений, деплоя
  • Требования не стандартизованы
  • Менеджерский долг перед заказчиком — сокрытие имеющихся на стороне команды исполнителя проблем, преувеличение успехов, новые обещания к старым долгам

Общий посыл, который бы я рекомендовал разработчикам, претендующим на роль «спасателя» (в должности сеньора, ведущего разработчика, техлида и т.п.): не врать и не бояться. Если вы застали проект в плачевном состоянии, это не значит, что люди, работавшие в нём до вас, считают так же. Во-первых: текущее состояние проекта во многом плод их усилий, людям психологически сложно воспринимать критику результата своего труда. Во-вторых: скорее всего, уровень команды, допустившей имеющиеся проблемы, не позволяет их разглядеть — для них объективно проблем не существует. В описываемом случае, банальные толстые контроллеры — содержащие всю бизнес-логику — считались приемлемыми. Отсутствие юнит-тестов — не воспринимается как что-то ужасное, если человек просто не знаком с модульным тестированием.

В подобной ситуации вам неизбежно придётся насаждать свои, лучшие практики в команде. Для этого есть два пути: эволюционный — обучить (в первую очередь собственным примером) имеющиеся кадры. Революционный — искать более компетентных исполнителей, а значит планомерно заменять команду.

Начинаем с тестов

Одним из первых моих мероприятий было подключение тестового фреймворка, помощь коллегам в запуске и написании тестов. Эволюция, конечно более гуманный вариант — не надо никого «сливать», тратить время и деньги на подбор более опытных (и вероятно — дорогих) специалистов. И кармически это гораздо полезней — делиться знанием. Но этот долгий путь. Если разработчик, достаточно зрелый, чтобы считать себя готовым к разработке корпоративных приложений за деньги, не освоил тестирование, что-то в его кривой профессионального роста пошло не так. Драма в том, что скорее всего, не выдержав новых требований, многие предпочтут найти новое место работы. Человеку свойственно идти по пути наименьшего сопротивления. В этом случае попытка обучить — пустая трата своего и чужого времени.

Если у вас нет в запасе лишнего человека-года, что более чем вероятно, для коммерческой разработки, то шансов плавно эволюционировать у малоопытной команды мало. Лучше сразу готовиться формировать новую команду, предъявляя к соискателям требования выше, чем было до этого. Да, увеличивая таким образом бюджет. И это не раздувание бюджета, это компенсация долга, созданного в прошлом менеджером: может в попытке сэкомить, может из-за недостатка опыта.

Перечисление технологических изменений, которые предстояло произвести, я начал с модульных тестов. Модульные тесты одновременно самая дешёвая во внедрении и сопровождении возможность для поддержания вашего проекта в порядке. И в то же время одна из самых фундаментальных. Начинать с тестов — хорошая привычка. С тестов можно начинать кодирование, использование незнакомой библиотеки или языка, постановку задачи, проверку реализации чего бы-то ни было. Тесты — это способ мышления, при котором вы, прежде чем сделать что-то, формально описываете результат. Если отточить этот навык до должного уровня, то получение любого результата не будет у вас вызывать затруднений, вы забудете про страх белого листа. И это касается не только программирования.

SVN -> Hg -> Git

Исторически на проекте использовался SVN. Мержи были делом сложным и ответственным, сопровождавшимся сакраментальным «не пуште пока в транк, сегодня деплоим». Коллеги по департаменту в других проектах использовали Mercurial. Около месяца мы пробовали эту систему контроля версий, но в итоге предпочли переход на хорошо знакомый и популярный Git. Как и ожидалось, большинство соискателей, при формировании новой команды, чаще оказывались знакомы с Git, нежели с двумя другими СКВ. Чем не повод для перехода?

CI & CD

Windows Server -> Ubuntu
Remote Desktop + manual update -> delpoy.sh -> Unix + Docker + TeamCity

Копии приложения для демонстраций и тестирования находились на Windows Server. Управление сервером и обновление приложений осуществлялось вручную, путём подключения к удалённому рабочему столу. Примерно пол-года мне понадобилось на убеждение менеджера, и, через него, заказчика, что обязательным предусловием выпуска в продакшн, должен стать перевод инфраструктуры на Unix. Параллельно с этим формальным обоснованием, в процессе поиска второго бэкенд-разработчика, я смотрел в сторону кандидатов владеющих администрированием LAMP стека. К счастью, нам удалось найти специалиста с хорошими навыками в Bash и Unix: в итоге он стал в команде на 50% разработчиком и на 50% билд- и интеграционным инженером. К выходу в продакшн у нас был полноценный CI и CD. Привет Rottenwood!

Это мероприятие, как прочие, не чисто техническое решение. Методологии и концепции разработки влияют на другие процессы, не забывайте об этом. Если менеджер привык руководить командой, для которой подготовка релиза сводиться к «**як-**як и в продакшн!», недостаточно настроить агент в TeamCity. Вам придётся донести до менеджера осознание того, что «нельзя просто взять и «пофиксить» это за пять минут на проде до демо». Да, это будет на первых порах доставлять дискомфорт. Менеджеру понадобиться месяц или больше, чтобы привыкнуть что деплой теперь происходит не по пинку, мгновенно, вместе с падением рабочего продакшена. Теперь это осознанная процедура, которая пусть занимает 10 минут, но гарантировано ничего не уронит, и даст предсказуемый результат на любом из серверов, сколько бы их у вас не было.

Для заказчика аргументами необходимости выделение на это ресурсов и времени послужили:

  • Снижение простоя в экстренных случаях — благодаря Docker мы можем оперативно развернуться на любой виртуальной машине в любом датацентре.
  • Мы можем поставить приложение вместе с образом и оно может быть запущено на любой машине (такой кейс тоже рассматривался) без участия со стороны команды разработки.
  • Безопасность — изолированность контейнера с приложением от хост-машины, простота и надёжность Unix для сервера. Часть пунктов при прохождении приложением корпоративного security-аудита автоматически закрывалась.
  • Родная среда для используемого приложением стека, шире набор применимых технологий для потенциальных фич. Большая лояльность разработчиков.
  • Ощутимый прирост производительности без дополнительной конфигурации.

Eclipse / NetBeans / Trial WebStorm / Brackets -> PhpStorm

Другим важным мероприятием стала организация приобретения для команды лицензии на PhpStorm и настройка единого стиля форматирования в соответствии с PSR. Я считаю любая организация может (и должна) обеспечить работников нормальными орудиями труда. PhpStorm и WebStorm сейчас лидирующие, на мой взгляд, IDE на рынке по поддержке PHP / JavaScript / TypeScript. Хорошая IDE существенно способна повысить как личную эффективность программиста, так и команды в целом — легко внедрить посредством настроек единый code style и разные полезные «примочки» для работы над проектом.

Devprom + Excel + *.jpg-> Jira

Этот переход, пожалуй оказался самым эпичным и долгожданным для нас. Исторически, использовался Devprom. Если в двух словах: никому не рекомендую эту систему. Для меня было откровением, что платное ПО может быть настолько низкого качества! Случайным образом система могла зависать, падать, содержала откровенные уязвимости. Каждый апдейт, помимо патча нескольких SQL-инъекций (и добавления новых, судя по частоте обновлений) привносил новшества в расположение элементов GUI, так что привычные уже сценарии использования приходилось осваивать заново.

Поскольку система управления проектом — краеугольный камень процесса планирования и разработки, использование «забагованного» и нестабильного решения сказывалось на всех остальных процессах: люди ленились заводить баги в трекере (это было сложно и долго), при планировании наполнение бэклога итерации превращалось в пытку на целый день, задачи терялись, инструменты аналитики и оценки было проще не использовать, менеджеры стремились при любой возможности протолкнуть в работу что-то в обход трекера. Таким образом используемый инструмент явно наносил вред нашим процессам, согласитесь это ужасно.

Благодаря Девпрому и изобретательности привычных к нему менеджеров, я наблюдал некоторые альтернативные способы управления:

  • Работа по картинкам — после тестирования на выходе имеем папку со скриншотами, названными в соответствии с приоритетами. Разработчики весело хватают понравившиеся картинки и «фиксят» их, перекладывая картинки в папочку Done. За попытку создать на каждую картинку задачу в трекере можно было получить по рукам.
  • Email-driven managment: в рассылке между членами команды на протяжении от нескольких дней до недели ходит письмо в таблицей высокоуровневых «фичей». Инициатива использовать доску трекера для оперативного управления разработкой — наказуема.
  • Excel planning — составление бэклога и оценка при помощи легендарного табличного процессора.

Трекер задач — не менее важен в разработке чем IDE. Это краеугольный камень процесса разработки: в нём должна начинаться и заканчиваться любая активность в проекте. Нет задачи — нет кода. Jira, возможно лучшее из решений для коммерческих организаций на рынке.

PM-level

Если разработчик осуществляет оперативное управление в команде, от его решений может зависеть многое. Его выбор определяет успех или провал реализации отдельных частей продукта. Конечно, это наиболее актуально для небольших команд: 2-4 разработчика, тестировщик, аналитик. Пропорционально увеличению количества разных специалистов — вводим архитекторов, администраторов, QA-отдел — надо полагать, степень персональной ответственности отдельного участника процесса снижается.
Но не забывайте, что есть как минимум два фактора, которые вы, будучи техническим специалистом, на которые у вас нет прямого влияния. При этом от них прямо зависит сама возможность успеха проекта:

  • Общий бюджет проекта и, соответственно, штатное расписание. Нельзя написать конкурентно-способный аналог приложения, если у вас в штате на порядок меньше специалистов. Дальше прототипа, на энтузиазме ваш «стартап» не улетит.
  • Деятельность стоящих перед вами в пищевой цепочке — заказчика и бизнес-аналитика.

Если менеджер проекта на вашей стороне и может выбивать у вышестоящего руководства (или заказчика, в зависимости от структуры вашей организации) дополнительные расходы, если менеджер способен отстоять перед заказчиком важность необходимых нефункциональных изменений в проекте и процессе — у вас есть шансы на успех. Остальное зависит от вашего совершенства.

Если менеджмент не готов прислушиваться к команде разработки, шансов на восстановление — нет. Вы не можете прямо влиять на решение вышестоящего, или сменить руководство проекта. Но вы должны сделать всё возможное чтобы вас услышали, если не хотите отвечать за чужие ошибки.

Dev-level

Если ваш опыт разработчика подсказывает что у проекта есть проблемы, например банальный технический долг, вы можете кинутся грудью на амбразуру рефакторинга… И погибнуть, поскольку остальная команда будет успевать «говнокодить» из своего пулемёта большее количество строк в минуту, чем вы сможете физически отрефакторить и покрыть тестами. Абстрагируйтесь от самой разработки, посмотрите на процесс с высоты птичьего полёта: обзоры кода, защищённые ветки и пул-реквесты, инструменты статистического анализа кода в вашем CI — есть множество инструментов позволяющих предотвратить распространение проблемы. Гораздо важнее устранить причину проблемы, устранение симптомов — дело вторичное. И не факт что у вас хватит времени на второе, с большинством legacy придётся жить ещё долго. Главное предотвратить метастазы.

Иногда ошибка действительно в генах. Тогда лучший способ — отсечь некачественный генотип от вашей популяции и заменить его свежим образцом с необходимыми доминирующими признаками.

После года рефакторинга, мне кажется что порой стоит оценивать разработчиков не большому по количеству принесённой пользы (функциональности + соответствующей кодовой базы), а по минимуму оставляемого вреда, в виде технического долга и неподдерживаемого, или не тестируемого кода. Конечно у вашего менеджера будет альтернативный взгляд на реальность. Но реальность разработки такова, что «запилить фичу» практически любой сложности для среднего разработчика не составляет труда. Гораздо важнее в средне- и долгосрочной перспективе чтобы это добавление не ухудшало архитектуру, сопровождалось не хрупкими и понятными тестами, было спроектировано с оглядкой на принципы SOLID. В этом отношении, я предпочту одно senior'a двум middle'ам и двух middle'ов четырём junior'ам. Чем длиннее дистанция, которую предстоит пройти вам и вашему продукту, тем важнее данный тезис.

Увы, собрать достаточно сильную команду в приемлемые сроки почти никогда не удаётся, даже обладая необходимым бюджетом. Квалифицированных специалистов даже в самых распространённых технологиях и фреймворках катастрофически не хватает. Построение процесса разработки на промышленном уровне поможет вам компенсировать это. Используйте возможные утилиты и техники для анализа и контроля качества кода. Поставив за отлаженный конвейер среднего или начинающего разработчика вы получите более предсказуемый результат и планомерное развитие, чем дав волю «коммитить» в мастер энтузиасту с горящими глазами. Ваша задача должна состоять в организации стабильного процесса и облегчении встраивания в него участников, а не героической ликвидации последствий не менее героических лихих кустарных решений.

Analytics

Если вы видите, что бизнес-аналитик скидывает в разработку сырые требования — не включайте фантазию и не начинайте кодирование. Составьте список вопросов и всех конфликтных мест и отправьте ему письмом. Или обсудите вместе над распечаткой все сомнительные моменты. Кодирование же стоит начинать тогда, когда у вас на руках есть настолько определённые требования, в утверждённом аналитиком файле, что задачу по их кодированию можно делегировать любому разработчику. Я считаю что идеально поставленная задача по разработке, кроме ссылки на релевантные требования, не должна содержать никакой «бизнес-логики», в крайнем случае высокоуровневое описание используемых шаблонов проектирования, если назначаемый разработчик ещё не знаком детально с компонентами системы, где предполагается внесение изменений.

Банальная истина, которой мы ежедневно забываем: люди склонны полагать свои мысли очевидными. Несмотря на средний высокий IQ в нашей отрасли, телепаты, увы, всегда в отпуске. Если вы войдёте в режим телепата и сделаете за аналитика его работу, то вы окажетесь виноваты в следующих грехах:

  1. С вероятностью близкой к единице, задача отнимет у вас времени больше, чем вы планировали. Ведь давая оценку, разработчик всегда озвучивает время на кодирование. Наиболее опытные из нас могут заложить риски отладки, тестирования, сопровождения документации и т.п. Но я сомневаюсь что даже лучшие из разработчиков способны точно прогнозировать время необходимое им на работу в качестве бизнес-аналитика. Аналитик не будет за вас программировать. А за превышенные сроки отвечать вам.
  2. Вы будете ответственны за все ваши фантазии, поскольку попадая в реализацию, они остаются не описанными в требованиях и не согласованными с заказчиком. Потом вам придётся долго убеждать всех, что это «не баг а фича», и в конечном итоге переписать этот код, с появлением реальных требований. Хорошо если вы же. Гораздо хуже наткнутся на недокументированное поведение и загадочный код с инициалами, владельца которых никто на проекте не помнит.
  3. Вы лишаете хорошего парня-аналитика возможности профессионального роста.
  4. Вы увеличиваете энтропию Вселенной, нивелируя выгоду от разделения труда.

Результаты устных обсуждений и переписки должны фиксироваться в конечных файлах с требованиями. Разработайте пакет документов, необходимых для адекватного планирования разработки и планомерного кодирования. Определите, какие форматы нужны для описания той или иной части системы. К примеру — для каждой экранной формы — может быть востребован следующий набор документов:

  • Файл Visio с описанием функциональности, блок-схемами зависимостей между полями
  • Файл Excel с правилами валидации и описанием типов данных для каждого поля
  • Файл PSD для верстальщика, сопутствующие исходники (шрифты, иконки)

Такой пакет будет востребован не только на этапе разработки но и на следующих: приёмочное тестирование и разработка пользовательской документации.

Идеально было бы версионировать эти файлы параллельно с исходным кодом, чтобы понимать актуальное и требуемое состояние системы. К сожалению, с большинством сложных офисным форматов версионирование по аналогии с исходными кодами в Git не применимо.

Бизнес-анализ — это процесс, который важно формализовать и отладить не меньше чем саму разработку. И поскольку в отношениях разработчик-аналитик вы являетесь своего рода потребителем, ваш долг помочь выстроить эти отношения к обоюдному удовольствию.

Estimate it

Если менеджер вынуждает вас давать оценку задачу, по которой не был проведён анализ, ставьте верхнюю из возможных. Я, например, для таких задач использую значения Фибоначчи 13, либо 21, в то время как для нормально спланированных задач максимальное значение 5. Таким образом вы явно отражаете сложность, которая на данном этапе не может быть оценена точнее.

Другая крайность: установите минимальную оценку. Я использую 1, хотя многие оптимисты склонны давать обещания вроде «это можно сделать за 5 / 10 /15 минут». Да, безусловно есть правки, внесение которых займёт считанные минуты — не считая накладные расходы на взаимодействие с трекером, СКВ, документаций, тестами. Чтобы не огорчать менеджера тем, что «маленький» фикс занимает целый инженерный час, могу порекомендовать связанные мелкие правки объединять в одну задачу.

QA

Если вы получаете баг-репорт в виде «Починить форму на странице M» или одного скриншота с большим жирным знаком вопроса на безобидной, всем привычной странице, у вас вряд ли получиться исправить проблему. Формализуйте формат баг-репорта в соответствии с особенностями вашего приложения. Покажите и расскажите всем причастным к тестированию продукта как получить отладочную информацию необходимую для исправления, как формулировать. Не пытайтесь воспроизвести невоспроизводимое.

Другой нюанс: если в команде нет культуры тестирования, менеджер может полагать что ручное тестирование продукта — дело разработчиков. Ваша миссия показать ему простую арифметику: час разработчика как правило в N-раз дороже часа «ручного» тестировщика. За несколько полных дней тестирования имеющимися в команде разработчиками, легко сжигается зарплата зарплата выделенного тестера. Не забываем о простое разработки.

Тестирование — это процесс, который должен проходить планомерно и на регулярной основе, а не мероприятие вроде апрельского субботника. Если у вас в организации ещё нет QA, но вы знаете что это такое — приближайте его появление всеми доступными средствами. Пока нет квалифицированного приёмочного тестирования, команда разработки будет оказываться крайней во всех обнаруженных багах. Если тестирование носит нерегулярный характер, то баги будут находиться редко, зато много и не те. Это значит, что лавина аврально обнаруженных багов будет отравлять вам жизнь, и серьёзно вредить всему процессу разработки. В упомянутом проекте, выбивание штатной единицы для QA-специалиста и поиск подходящего кандидата у меня заняло около полутора лет.

Какие риски несёт нерегулярное и плохо организованное тестирование:

  • Если тестирование проводиться редко, будет казаться что версия ужасно «забагована», кровь-кишки-чума, а-а-а всё пропало, всё бросаем, все бежим на пожар!!1 — это срывает запланированную разработку, порой полностью парализуя её. Риск сорвать следующий дедлайн.
  • Единовременное обнаружение большого количества багов, субъективно будет восприниматься, как то что их критически много в (казалось бы) готовой версии, что подрывает ваш авторитет разработчика.
  • В случае с ручным, эпизодическим, слабо организованным тестированием баг-репорты будут низкого качества — а значит трудно-воспроизводимы, неактуальны, дублированы. Разработчики будут тратить дополнительные усилия и время на попытки их воспроизведение. По моему опыту могу сказать, что, зачастую, воспроизведение бага занимает времени не меньше чем исправление. И вдвойне обидно за потраченное время, если воспроизвести не удалось.
  • Время на исправление большого количества багов сложнее оценить, по сравнению с регулярным ежедневным включением их в план. Если процесс отлажен, то, располагая метриками производительности тестирования и разработки, можно закладывать оценку исправлений в долгосрочный план, параллельно с остальной разработкой.
  • Шансов «пофиксить» все ставшие известными за день до релиза баги в срок будет очень хотеться и менеджеру и молодым разработчикам. Но нет, так не бывает. Релиз будет либо ненадлежащего качества, либо сорван.
  • Специалисты, выполняющие тестирование от случая к случаю (приходящие стажёры, проектные менеджеры, аналитики, сами разработчики (упаси бог!), кофе-леди, бухгалтера и сантехники) по определению: а). низко-эффективны в этой роли, поскольку она для них эпизодическая б). вряд ли будут действовать согласовано по какому-либо плану или сценариям в). дадут гарантировано некачественные отчёты

Помочь организовать здоровое тестирование — в собственных интересах разработчика.

Лучшее — главный враг хорошего!

Если вам повезёт и вы отладите разработку до идеального состояния, это не значит что менеджмент и заказчик автоматически станут счастливы. Во-первых, если PM, например, хронически прогибается под заказчика, беря без ведома команды обязательства по разработке больше чем физически можно успеть, то по мере роста стабильности и производительности разработки, будут расти и обязательства. Во-вторых, всегда найдутся не озвученные раньше проблемы, которые за не имением прочих получат самый высокий приоритет. Здесь схема такая: если раньше команда «факапила» стабильность и новую функциональность, то теперь заказчик может посчитать что приложение медленное и записать это как «эпичный факап», хотя изначально никаких требований относительно этого не существовало. Или вспомнить некую Pet-feature, ломающую всю имеющуюся логику и выставить её как must-have, а любые контр-аргументы посчитать непрофессионализмом и саботажем. Тут уже всё от адекватности заказчика и вашей менеджерской прослойки зависит. В таких ситуация остаётся только последовательно аргументировать, взывать к логике или искать более благодарную организацию.

Ещё один нюанс, о котором стоит помнить: по мере роста продукта, стоимость внесения изменений неизбежно растёт. Всегда. Все лучшие практики и прочие примочки, о которых написано выше, лишь уменьшают коэффициент роста этой стоимости. Таким образом, если человек не имел продолжительного опыта работы с командами, которые имеют хорошо поставленные процессы, и плохо эти процессы понимающий, он может сделать следующий вывод:

— было: никакого формализма, **як и в прод, пацаны быстро делали всё.
— стало: куча формализма, CI/QA-какой-то мешает быстро «релизить», пацаны медленно стали кодить…

При этом, не имея личного опыта работы в проектах различных размеров и командах, от него ускользает, что между «было» и «стало» находятся:

  1. большой временной период
  2. большой срез функциональности реализованной за это время
  3. не выплаченные долги из того что «было»

Поэтому, для таких персонажей важно не столько само наведение порядка — для не-разработчика, любые изменения, кроме функциональных, будут эфемерными и необязательными. Важно аргументированное обоснование с наглядными примерами: что, как и почему. Тот самый аспект профессии разработчика, о котором упоминает Макконнелл.

Автор: samizdam

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js