Медленно и стабильно: преобразование всего интерфейса Sentry в TypeScript
Недавно Sentry преобразовала 100% своей клиентской базы кода React с JavaScript на TypeScript. В этом году работа охватила более десятка членов команды инженеров, 1100 файлов и 95 000 строк кода.
В этом сообщении блога мы делимся своим процессом, методами, проблемами и, в конечном итоге, тем, что мы узнали на этом пути.
Начало
Еще в 2019 году мы отправляли больше ошибок внешнего интерфейса, чем было допустимо. После изучения основных причин этих инцидентов стало ясно, что многие из этих ошибок можно было предотвратить с помощью статического анализа и проверки типов.
Во время мероприятия Hackweek того года Лин Нагара, Альберто Леал и Дэниел Гриссер представили TypeScript для интерфейса Sentry. Эта команда загрузила компилятор TypeScript в наш процесс сборки, а также преобразовала несколько нетривиальных представлений - и связанных с ними компонентов - в TypeScript.
Hackweek - это мероприятие, которое проводится один раз в год, давая всем сотрудникам Sentry возможность отложить свою обычную работу и сосредоточиться исключительно на инновационных проектах и идеях. Hackweek породил множество приложений и инструментов, которые сейчас являются важными частями нашего продукта, например, недавно запущенный проект Dark Mode.
После рассмотрения презентации мы пришли к выводу, что Typescript отлично подходит для Sentry, потому что:
- Во время компиляции можно было обнаружить и устранить несколько классов ошибок.
- Мы могли бы улучшить взаимодействие с разработчиками за счет интеграции редакторов, таких как автозаполнение, более быстрая навигация по коду и встроенная обратная связь компилятора.
- Мы могли бы уменьшить потребность в документации API, поскольку аннотации типов помогают создавать код с самоописанием.
- TypeScript имеет активное сообщество с четкой и поддерживаемой дорожной картой разработки в дополнение к быстрым выпускам.
- Многие из используемых нами библиотек (включая React) уже имеют определения типов.
- TypeScript можно внедрять постепенно. Это означало, что мы можем начать писать новый код с помощью TypeScript и постепенно преобразовывать его со временем.
Однако у внедрения TypeScript были некоторые потенциальные недостатки:
- Это большие временные затраты. Наш код внешнего интерфейса не является тривиальным по своему охвату, поэтому для его преобразования потребуются значительные усилия. Эта сложность означала дополнительное время сборки.
- Нам нужно будет обучить команду внешнего интерфейса TypeScript и поддержать их в процессе обучения.
- TypeScript и JavaScript должны сосуществовать в кодовой базе в течение значительного периода времени.
Созревание прототипа
Вскоре после Hackweek был большой ажиотаж, и в наш Технический руководящий комитет (TSC) было внесено более формальное предложение. Эта группа собирается каждые две недели, чтобы руководить нашей интерфейсной архитектурой. Хотя TypeScript не был среди «выигрышных» проектов для Hackweek, мы были уверены, что это будет стоящая инвестиция, которая в конечном итоге окупится в долгосрочной перспективе.
Общая стратегия
Мы разбили нашу высокоуровневую стратегию на несколько этапов:
- Обучение. На этом этапе нам нужно было сообщить людям о начале работы с TypeScript и предоставить необходимые учебные ресурсы, чтобы помочь им освоиться.
- Новый код в TypeScript. На этом этапе нам нужно было, чтобы все новые разработки выполнялись на TypeScript. Если мы продолжим создавать новый JavaScript, мы никогда не завершим этап преобразования.
- Конверсия. На этом этапе вся новая работа будет выполняться на TypeScript, что дает нам конечное количество файлов для преобразования. Тогда это «просто работа» ™.
Нашим самым спорным решением было согласие не подвергаться каким-либо другим серьезным рефакторам до тех пор, пока кодовая база не будет полностью преобразована в TypeScript. Это означало, что мы не будем заниматься другими улучшениями качества жизни - такими как обновление нашей библиотеки управления состоянием или введение перехватчиков React - до тех пор, пока преобразование TypeScript не будет завершено.
Обучение команды
С самого начала мы поняли, что более широкой команде разработчиков в Sentry потребуются дополнительные ресурсы и материалы для изучения TypeScript. Чтобы помочь людям, которые плохо знакомы с TypeScript, мы поделились списком вводных статей и ресурсов для настройки различных редакторов.
Кроме того, члены TSC нашли время, чтобы проанализировать код и помочь обучить тех, кто хочет изучить TypeScript. Наличие этой системы поддержки помогло создать больше «сторонников» TypeScript, которые со временем будут писать новый код на TypeScript.
Укорениться в зеленых полях
Пока мы обучали более широкую команду, люди, увлеченные TypeScript, не только начали создавать свои новые функции в TypeScript, но также нашли возможности конвертировать файлы, которые перекрывались с новыми функциями. Такой подход позволил нам создать наши определения типов и получить больше опыта в написании Typescript в частях продукта с меньшим риском, которые не были доступны клиентам.
По мере того, как более широкая команда набиралась опыта и ценила то, что предоставляет TypeScript, они, естественно, перестали создавать больше JavaScript. Хотя мы никогда не использовали инструменты, чтобы помешать людям создавать новый JavaScript, наши образовательные усилия и социальные соглашения помогли предотвратить создание нового JavaScript.
Тренируйтесь от ядра - и от листьев
Когда TypeScript прочно обосновался, нам понадобилась стратегия для работы с более чем 1100 файлами, которые требовали преобразования. Здесь мы проверили наш импорт, упорядочив его по частоте импорта каждого модуля. Мы использовали этот список, чтобы определить, какие модули были преобразованы в первую очередь. Преобразовывая часто используемые модули, мы могли бы увеличить нашу прибыль по мере преобразования файлов.
Этот подход хорошо работал вначале, так как некоторые модули имеют значительно больше импорта, чем другие. Но поскольку большинство наших модулей имеют менее 10 операций импорта, мы быстро остановились. Наш следующий подход начинался с модулей «листовых узлов», которые импортируются в одном месте. Преобразование этих файлов позволило нам быстрее накапливать прогресс.
Все, что нам нужно сделать, это преобразовать более 1 100 файлов…
Как и во многих других программных проектах, наши первоначальные планы развертывания были слишком амбициозными. Мы начали с ретроспективного расчета графика, который мы завершили в течение 2019 года. Примерно за 15 недель до конца года это означало, что нам потребуется преобразовывать примерно 74 файла в неделю. Это предполагало, что мы не будем накапливать никаких дополнительных файлов JavaScript (мы это сделали) и что мы сможем выдержать эти усилия (мы этого не сделали). Через восемь недель мы проверили наш прогресс.
Было очевидно, что мы не собираемся завершить его в 2019 году. Учитывая прогнозы текущих усилий, более вероятной датой завершения будет середина 2020 года.
Осенью и зимой 2019 года прогресс был медленным. Люди были сосредоточены на достижении целей продукта, и у них не было так много времени на преобразование TypeScript. В феврале 2020 года мы достигли равновесия. Мы больше не создавали новый JavaScript, и наша работа по преобразованию была исправлена.
Встречные вызовы
Хотя внедрение TypeScript определенно изменило правила игры, мы также столкнулись с некоторыми проблемами в процессе преобразования. Большинство из них было связано с проблемами взаимодействия между TypeScript и React:
1. Реквизит по умолчанию
При использовании defaultProps
в классах, TypeScript может правильно сделать вывод, что реквизиты не требуются при использовании компонента, но при использовании компонентов более высокого порядка типы для defaultProps
обычно не работают, и ранее необязательные свойства станут обязательными.
Пример того, как defaultProps
плохо взаимодействует с компонентами более высокого порядка:
const defaultProps = {
statsPeriod: DEFAULT_STREAM_GROUP_STATS_PERIOD,
canSelect: true,
withChart: true,
useFilteredStats: false,
};
type Props = {
id: string;
selection: GlobalSelection;
organization: Organization;
displayReprocessingLayout?: boolean;
query?: string;
hasGuideAnchor?: boolean;
memberList?: User[];
onMarkReviewed?: (itemIds: string[]) => void;
showInboxTime?: boolean;
index?: number;
} & typeof defaultProps
type State = {...};
class StreamGroup extends React.Component<Props, State> {
static defaultProps = defaultProps;
...
}
export default withGlobalSelection(withOrganization(StreamGroup));
Обычно TypeScript может использовать атрибут defaultProps
нашего компонента класса, чтобы сделать вывод, что эти свойства не требуются. Однако, если TypeScript заключен в компонент более высокого порядка, он отображает следующие ошибки:
Здесь наше решение было использовать Partial
на defaultProps
и полагаться на React для заполнения значений по умолчанию.
const defaultProps = {
statsPeriod: DEFAULT_STREAM_GROUP_STATS_PERIOD,
canSelect: true,
withChart: true,
useFilteredStats: false,
};
type Props = {
id: string;
selection: GlobalSelection;
organization: Organization;
displayReprocessingLayout?: boolean;
query?: string;
hasGuideAnchor?: boolean;
memberList?: User[];
onMarkReviewed?: (itemIds: string[]) => void;
showInboxTime?: boolean;
index?: number;
} & Partial<typeof defaultProps>
type State = {...};
class StreamGroup extends React.Component<Props, State> {
static defaultProps = defaultProps;
...
}
export default withGlobalSelection(withOrganization(StreamGroup));
Вы можете найти более полную реализацию этого подхода здесь.
2. Библиотеки, добавляющие неправильные типы
Одним из недостатков использования определений типов в DefinuneTyped является то, что иногда типы библиотеки не пишутся сопровождающими. Вместо этого пользователи сообщества вносят типы, и из-за этого некоторые типы отсутствуют или определены неправильно. Мы столкнулись с этим с версиями ECharts и Reflux, которые мы использовали. Нашим решением здесь было добавление дополнительных определений типов в наш код.
3. React.forwardRef несовместим с Generics
Использование универсальных типов с React.forwardRef
невозможно напрямую, так как для этого требуются конкретные типы. Более подробно, функция forwardRef
имеет только один параметр с именем render
. Тип этого параметра ForwardRefRenderFunction
не является объявлением универсальной функции, поэтому вывод типа функции более высокого порядка не может распространять параметры свободного типа на вызывающую функцию React.forwardRef
. Когда возникла такая ситуация, нам пришлось идти на компромиссы и использовать «любое».
Поддержание мотивации и энергии
Ближе к концу преобразования многие участники чувствовали, что над этим проектом был тяжёлый труд.
Летом 2020 года - через год после начала этого проекта - мы перешагнули 70-процентный порог. Это оживило людей, поскольку мы знали, что конец близок. Мы смогли сохранить эту энергию и сосредоточиться все лето и осень, используя часть нашего собрания TSC в качестве проверки и сбора «обещаний конверсии» для следующего собрания. Это представило беззаботную социальную игру, которая помогла нам сосредоточиться.
Кроме того, наша фантастическая команда разработчиков представила Slackbot, который позволит нам отслеживать прогресс по запросу. Наблюдение за тем, как число растет каждый день, было большим мотивом на заключительных этапах, настолько, что мы, вероятно, воспользуемся этим снова. Вы можете найти ранние версии этого бота здесь.
И наконец
После 18 месяцев миграции нашей клиентской базы кода на TypeScript, наконец, настал день, к которому все в Sentry стремились. Когда мы начали свое путешествие по TypeScript, нам нужно было преобразовать более 1100 файлов. Теперь у нас есть более 1915 файлов Typescript. Стоит отметить, что ни разу не добавлялась проверка GitHub для блокировки новых файлов JavaScript. После того, как разработчики увидели преимущества TypeScript, естественным выбором стало написание нового кода на TypeScript.
С TypeScript у нас теперь есть дополнительный уровень защиты в нашем коде, что означает, что мы можем поставлять с большей уверенностью, более высокой производительностью и, что самое важное, с меньшим количеством ошибок. Некоторые из наших новых разработчиков внешнего интерфейса никогда не сталкивались с производственными инцидентами, вызванными изменением внешнего интерфейса.
Оглядываясь назад
Как и все в жизни, мы также узнали несколько вещей на этом пути.
1. Дополнительная конверсия - ключ к успеху
Наша стратегия постепенного переноса файлов на TypeScript сработала. Мы смогли сбалансировать преобразование нашего кода в TypeScript, не откладывая важную работу над продуктом. Важно подчеркнуть, что с самого начала мы не торопились достичь своей цели, но вместо этого мы хотели действовать осторожно и проделать большую работу.
2. Будьте в курсе выпусков TypeScript
В процессе преобразования вышло несколько новых версий TypeScript. Каждый из них помог нам еще больше уточнить наши типы с помощью новых функций, таких как необязательное связывание, объединение с нулевым значением, именованные кортежи и многое другое. Хотя обновление потребовало дополнительных усилий, преимущества того стоили. Вот почему мы рекомендуем оставаться в курсе последних выпусков TypeScript.
3. Постепенно создавайте сложные типы.
В начале миграции было невозможно определить правильный тип всего. В конце концов, Sentry обладает большой кодовой базой, и не все знакомы со всеми частями приложения. Это означало, что нам приходилось создавать более сложные типы постепенно. По мере преобразования файлов мы лучше знакомились с их типами, а по мере преобразования связанных файлов мы могли лучше определять, были ли типы, которые мы ранее определили, обновлены с учетом новых данных.
4. Используйте комментарии TODO, чтобы отметить будущую работу
В Sentry мы обычно используем комментарии TODO в коде, чтобы помочь нам отследить то, что нам нужно просмотреть позже. Этот подход оказался очень полезным при переходе на TypeScript. Когда мы сталкивались с неясным типом или проблемным компонентом, мы оставляли его TODO(ts)
на рассмотрение позже. Сейчас мы постепенно пересматриваем список TODO и дорабатываем и улучшаем наши типы.
import {Frame} from 'app/types';
// TODO(ts): define correct stack trace type
function getRelevantFrame(stacktrace: any): Frame {
if (!stacktrace.hasSystemFrames) {
return stacktrace.frames[stacktrace.frames.length - 1];
}
for (let i = stacktrace.frames.length - 1; i >= 0; i--) {
const frame = stacktrace.frames[i];
if (frame.inApp) {
return frame;
}
}
// this should not happen
return stacktrace.frames[stacktrace.frames.length - 1];
}
export default getRelevantFrame;
Движение вперед
Переход на TypeScript был только началом. Команда веб-интерфейса Sentry продолжит постепенно улучшать типы, проверяя их правильность, включая удаление всех React PropTypes.
Мы также серьезно рассматриваем возможность внедрения сквозной безопасности типов, чтобы бэкэнд-инженер мог вносить изменения в API, не подозревая, что нарушает работу клиентов, а фронтенд-инженеры могли быть уверены в данных, которые будут возвращаться с сервера.
Это важное достижение было бы невозможно без терпения, настойчивости, внимания к деталям, энтузиазма и упорного труда всех участников. Большое спасибо всем Sentaurs, которые внесли свой вклад в это огромное усилие