Василий Половнёв

Ведущий разработчик.

Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.

Связаться: vasily@polovnyov.ru или телеграм.

Блог Курс о тестах в Руби и Рельсах

Гипотеза: мы охереваем, потому что слишком легко

Есть гипотеза. Мы охереваем от перегруза, потому что понабрали слишком много обязательств. Эти обязательства тянут за собой планирование, коммуникацию и переключение доменных областей. Это и дает нам чувство разбитости и усталости.

Попробую показать на примере. Представим, что я подписался запрограммировать встраиваемый платежный виджет, сделать код-ревью в соседнем проекте о барсуках, выполнить мастера спорта по тяжелой атлетике, самостоятельно вести бухгалтерию в ИП и оптимизировать билд на CI, чтобы он укладывался в 3 минуты.

Каждый из этих проектов тянет за собой свою доменную область, которую надо восстанавливать в памяти. Тут у нас мерчанты и платежи. Тут барсуки, передержки и ветеринарки. Тут рывок, толчковая тяга и подрыв. Тут УСН, ПСН, счета, акты и ЭДО. Тут CI, yaml и профайлер. Любой, кто пробовал быстро переключить внимание между барсуками и УСН, знает, что это непросто. Мозг тупит и кипит минут 15-20. Есть риск задекларировать барсука.

Также каждый из этих проектов тянет за собой коммуникацию и планирование. Что там по дедлайнам у виджета? Нужно попросить у дизайнеров фотки барсуков на передержке. Не забыть зарегаться на соревнования по ТА. Акт не прошел в СБИС через роуминг, надо написать в техподдержку. Надо показать ПР с ускорением билда ребятам, чтобы покритиковали. Это тоже работа, которая также требует времени и внимания.

И кажется, что мы понабрали так много всего, потому что это «легко». Легко ревьюить соседний проект, это же не код писать. Легко выполнить мастер спорта по ТА, просто делай, что в программе написано. Легко вести ИП, в Эльбе уже все есть, только кнопки нажимай. Легко оптимизировать билд на CI, ты же программист.

Сколько обязательств вы взяли, потому что это казалось «легко»?

Сформулируй просьбу или вопрос

Вот сидишь ты в собственном кубикле, работаешь там, тут остановился и увидел сообщение в чате:

Джон, пересматриваю все формы, а у меня почему-то локально на десктопе с последней версией мастера формы распёрло. Внутри вёрстка такая же, как и была.

И чё? Что с этим делать-то? Куда бежать? Чем помочь?

Чтобы не тратить время и внимание коллег на лишние ДУМАНИЯ, лучше всё рабочее общение сводить к двум типам сообщений: просьбам и вопросам. Соответственно, сообщение выше лучше переформулировать в вопрос:

Пересматриваю формы, а у меня на десктопе с последним мастером распёрло формы. Вёрстка не менялась. Что бы это могло быть? Что из последних изменений могло повлиять?

Или в просьбу:

Пересматриваю формы, а у меня на десктопе с последним мастером распёрло формы. Верстка не менялась. Помоги, пожалуйста, отдебажить

Также и с другой стороны. Если вам пишут непонятное и загадочное, попросите сформулировать просьбу или вопрос.

Дефолтный box-sizing в ЦСС. Спасибо, ИЕ!

Дефолтный box-sizing в ЦСС. Спасибо, ИЕ!

Все знают, что верстку нужно начинать с заклинания, переключающую блочную модель с инопланетной на человеческую:

*,
*::before,
*::after {
  box-sizing: border-box;
}

Если этого не сделать, будет работать значение по умолчанию, box-sizing: content-box, то есть падинги и бордеры не будут входить в ширину элемента. Соответственно, если сказать инпутам width: 100%, они переполнят форму, потому что их ширина будет равна 100% + падинги + бордеры.

Мне такое поведение всегда казалось всратым. Если я измеряю коробку для обуви, то ее ширина — это ширина коробки, а не обуви внутри.

Оказывается, появлением вменяемой блочной модели мы обязаны Интернет Эксплореру:

Specifically, IE was treating width to include the border and the padding while CSS1 treated width as including only the content. This became known as the “IE box model”.

In 1998 the Web Standards Project compiled a list of IE’s many CSS failings, including this one.

Еще чуть больше подробностей:
https://www.jefftk.com/p/the-revenge-of-the-ie-box-model

Ищу сообразительного стажёра

Ищу сообразительного стажёра, который вместе со мной будет делать крутые бэкендерские штуки и со временем станет крепким медлом.

Я ищу стажёра, который:

  • отличает GET от POST, SELECT от UPDATE, SSH от zsh;
  • отличает git rebase от git merge, пулреквест от реверта;
  • знаком с Рельсами на уровне «написал блог за 15 минут, а потом допиливал его по выходным»;
  • слышал про RabbitMQ, RSpec, модульные и интеграционные тесты;
  • не боится ни легаси, ни ПХП, ни фронтенда, ни асинхронных систем.

За полтора месяца научу стажёра:

  • проверять свою работу модульными и интеграционными тестами;
  • писать тесты быстрее, чем код;
  • ревьюить, парно программировать и не унывать;
  • быстро работать с Гитхабом, Рельсами и Семафором;
  • находить и исправлять «запахи» в коде: от Long Method до Shotgun Surgery.

Для этого стажёр будет:

  • работать 15-20 часов в неделю, часть времени — в паре со мной;
  • тратить 2-3 часа в неделю на теорию и домашние задания;
  • переписывать код снова и снова после ревью.

После стажировки мы либо продолжим работать вместе, либо стажер уйдёт с планом роста и списком рекомендуемой литературы.

Если написанное выше про вас, напишите мне письмо с рассказом о себе: vasily@polovnyov.ru

Стыдиться старого кода — ок

Если открываю код, написанный мною полгода-год назад, и мне не стыдно за него, то это плохой знак. Скорее всего, я не развиваюсь. Скорее всего, это не 5 лет опыта, а 1 год, повторённый 5 раз. Скорее всего, меня вот-вот выпрут из профессии и отправят программировать на Битриксе или 1С (no offence). А если серьёзно, то «стыд» за старый код должен появляться по двум причинам.

Во-первых, я могу вырасти как программист. Тут удачнее был бы паттерн А, тут лучше бы всё завернуть в консёрн Shareable, а вот здесь надо быть осторожнее с таймаутами на той стороне.

Во-вторых, я могу вырасти как специалист, набраться продуктового опыта. Тут мы зря разрешили кастомизировать урлы, все пользуются дефолтными. Зря сразу не сделали купоны с фиксированной ценой. Жаль, не предусмотрели, что люди могут дарить N подарков за раз.

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

3д-печать и ФФФ

В июле мой дорогой брат Ниязыч подарил мне 3д-принтер, мойку-сушку и два литра смолы. С тех пор я открыл 3д-типографию на балконе и стал 3д-издателем. Печатаю детские игрушки: котиков, единорогов, драконов и накачанных Пикачу.

У меня фотополимерный 3д-принтер. Он печатает с помощью фотополимерной смолы. Смола по умолчанию жидкая, но если посветить на нее особой длиной волны, затвердевает. Принтер печатает модельку слой за слоем, «запекая» их на платформе. Я наливаю смолу в ванночку с прозрачным дном, принтер опускает в нее платформу и засвечивает через дно текущий слой. Слой затвердевает и прилипает к платформе. Принтер поднимает платформу, отрывая запекшийся слой, и повторяет процесс для следующих слоев.

У такого способа печати бывают проблемы. Скажем, если слой слишком большой, принтер не сможет его оторвать и он останется на дне ванночки, похерив все последующие слои. Если слою не к чему прилепиться, если нет подпорок, он отвалится, станет плавать в ванночке и портить последующие слои.

При этом цена ошибок высока. Скажем, котик печатается 12 часов. Если вовремя не заметить проблему, легко похерить смолу и 12 часов печати. Еще и принтер придется «сбрасывать»: очищать ванночку, убирать с нее проблемный слой, фильтровать остатки смолы и все чистить.

Чтобы вовремя замечать проблемы и не терять время, я использую «Гвозди в гусенице»:

  1. Ставлю котика на печать
  2. Прошу Сири напомнить мне проверить котика через час
  3. Когда час прошел, должно быть готово около сантиметра модельки. Ставлю принтер на паузу, поднимаю платформу и изучаю результат. Если все в порядке, продолжаю печать. Если нет, останавливаю печать, переделываю модель и ставлю на печать новую ревизию
  4. Прошу Сири напомнить мне проверить котика через 5 часов
  5. Когда прошло еще 5 часов, должна быть готова половина модельки. Ставлю принтер на паузу, поднимаю платформу и изучаю результат. Если все в порядке, продолжаю печать. Если нет, останавливаю печать, переделываю модель и ставлю на печать новую ревизию

90% проблем ловятся на первом гвозде. Если первые слои не смогли закрепиться, то и печатать дальше нет смысла, поэтому первый гвоздь — как можно ближе к началу. Оставшиеся проблемы ловятся на втором гвозде, половине модельки.

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

Гвозди работают везде. Поручили младшему разработчику задачу на 5 дней, договоритесь о гвоздях. Через день проверьте направление мысли и модель решения задачи, через 3 дня — готовность половины работы. Если что-то пойдет не так, вовремя заметите и скорректируете.

Баготерапия

Мне тяжело неделями пилить новые фичи. Фичи связаны с неопределенностью — мы не совсем понимаем, куда идем, что нас ждет впереди, что мы забыли или не учли. Короче, очень много энергии уходит на ДУМАНИЯ. Если пилить новые фичи 6 недель подряд, можно здорово выгореть.

С другой стороны, мне легко исправлять баги. Баги тоже связаны с неопределенностью, но другого вида — мы не понимаем их причину. Зато мы знаем, как работает сейчас и как должно работать. Бац, бац и багфикс готов. Еще полчаса и он уже в продакшене. Красота!

Поэтому я предпочитаю чередовать фичи и баги. Когда чувствую, что не могу больше пилить новое, переключаюсь на баги и отдыхаю. Это работает на уровне итераций и ежедневной работы. В первом случае после 6 недель работы над новыми фичами, мы планируем разгрузочную неделю на рефакторинг и багфиксы. Во втором случае я каждый день беру баг-два, чтобы передохнуть. Периодизация!

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

Дефолтные стили браузеров

Если сделать текстовый файлик, написать в него немного текста и тегов и открыть в браузере, то текст все равно будет стилизован. Индексы работают, задан какой-то кегль и интерлиньяж.

Так происходит потому, что у браузеров есть user agent stylesheets — цсс-стили, встроенные в сам браузер. То есть это не какое-то дефолтное поведение отдельных текстовых нод, а буквально цсс-файл, который браузер подключает в первую очередь ко всем страницам. И конечно, именно с этих стилей начинается каскад: стили браузера → стили страницы → стили пользователя.

Такое поведение — часть спецификации CSS2:

User agent: Conforming user agents must apply a default style sheet (or behave as if they did). A user agent’s default style sheet should present the elements of the document language in ways that satisfy general presentation expectations for the document language (e.g., for visual browsers, the EM element in HTML is presented using an italic font). See A sample style sheet for HTML for a recommended default style sheet for HTML documents.

И конечно, эти стили можно посмотреть: опен-сорс, все дела. Например, Хром:
https://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css

Сафари:
https://trac.webkit.org/browser/trunk/Source/WebCore/css/html.css

Фаерфокс:
https://searchfox.org/mozilla-central/source/layout/style/res/html.css

Эти стили стоит глянуть, чтобы офигеть (1000 строк!), лучше понять ЦСС (head { display: none }) и подсмотреть прикольные трюки.

Просто не делай херни

Бывает, в сложных и проблемных ситуациях хочется просто забить и ультануть: внезапно уволиться с работы и стать плотником, бросить семью и уйти в ЧВК, написать клиенту, что умерла бабушка, и пропасть, пушнуть в мастер с форсом. В таких случаях ты получаешь мгновенное облегчение, а с другой стороны, просто сваливаешь, вывалив ушат говна на тех, кто на тебя надеялся. Совсем не круто.

В последний год я научился справляться с такими желаниями с помощью мантры «Просто не делай херни и…». Жопа на работе — просто не делай херни и подумай, как разгрести. Жопа в семье — просто не делай херни и подумай, как восстановить отношения. Не успеваешь с проектом — просто не делай херни и поговори с клиентом. Хочешь форспушнуть в мастер — просто не делай херни и разрезолви конфликт. Короче, просто не делай херни.

Знающие люди подсказывают, что это называется «Профессиональное самоубийство».

Как конкретно разгребать ситуации, кажущиеся неподъемными и ужасными — тема отдельного поста. Если коротко, то нужны две вещи: уверенность в том, что проблемы решаются делом, и «паззлы».

Датчик есть, но его как бы нет

В ноябре я попал в жесткое ДТП и серьезно поехал кукухой по безопасности. Исключением не стал и дом: я накупил огнетушителей, детекторов дыма и датчиков протечки. Один из датчиков бросил под ванну, убедился, что он работает, и забыл.

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

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

Что с этим можно поделать? Во-первых, убедиться, что датчики проверяют то, что нужно. Во-вторых, убедиться, что датчиков хватает. В-третьих, ввести регулярные «проверки связи» и sanity checks.

Большая маленькая ложь

Самая большая ложь в разработке — фраза «сделаем когда-нибудь». Конечно, никто ничего никогда не сделает. Это прекрасная двойная ложь: врем себе и остальным, будто задача важна, и врем себе, что обязательно до нее доберемся.

Если у задачи нет дедлайна, считайте, что и задачи нет. Поэтому любое «сделаем когда-нибудь» нужно либо фиксировать дедлайном, либо отпускать с миром: значит, не очень-то оно нам и нужно.

P. S. Даже если добавить «после пуска», «после майских» или «после отпусков», ситуация не изменится: никто ничего не сделает, все забудут.

Карма тестировщика

Бывают люди с кармой тестировщика: что бы они не использовали, находят самые невероятные баги, заусенцы и нестыковки. Взялся пинговать ya.ru, сломал ping ¯\_(ツ)\_/¯. Самые суровые из них одарены еще и бесконечной энергией: 5 багрепортов за 10 минут для них не проблема.

Конечно, когда я был начинающим программистом, я всегда их недолюбливал: ну емана, неужели тяжело НОРМАЛЬНО пинговать? Зачем это исправлять, почему нельзя последовательно нажать A, B и C, но с минимальной задержкой в 300 мс?

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

irb: echo on assignment

Недавно заметил, что при запуске рельсовой консоли пропало «эхо» при присвоении. Раньше было так:

> subscription = Subscription.find(123)
=>
#<Subscription:0x00007fe17d6b6e38
 id: 123,
 product_id: "xxx",
 status: "active">

А стало так (некруто):

> subscription = Subscription.find(123)
=>
#<Subscription:0x00007fb3f4d06588
...

Полез разбираться и узнал пару интересных вещей. Во-первых, с марта railties бандлит irb.

Это нужно для того, чтобы ребята со старым Руби все равно получили новый irb вместо «системного» старья.

Во-вторых, новый irb по умолчанию обрезает «эхо» при присвоении:

В-третьих, чтобы вернуть «эхо», достаточно подкрутить конфигурацию irb в ~/.irbrc:

IRB.conf[:ECHO_ON_ASSIGNMENT] = true

Там же можно вырубить всратое автодополнение:

IRB.conf[:USE_AUTOCOMPLETE] = false

Как тестировать код, завязанный на рандом

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

class Banner
  attr_reader :probability

  def initialize(probability:)
    @probability = probability
  end

  def visible?
    probability > Kernel.rand
  end
end

Чтобы проверить visible?, можно провести достаточно большое количество испытаний и оценить получившийся процент появлений банера:

banner = described_class.new(probability: 0.7)

# Проводим 1000 испытаний, результаты — в массив
trials = Array.new(1000) { banner.visible? }
# Сколько раз visible? вернул true
successful_trials = trials.select { |trial| trial }
# Какой процент
successful_trials_percentage = successful_trials.length.to_f / trials.length.to_f

# Проверяем получившийся процент
expect(successful_trials_percentage).to be_within(0.05).of(0.7)

Этот вариант кажется логичным и математичным. Но с ним есть проблемы: «мигания» и производительность. Тест будет иногда падать в зависимости от количества испытаний: чем меньше испытаний, тем чаще. Чтобы сделать тест стабильным, придётся увеличить количество испытаний и завернуть тест в rspec-retry.

С другой стороны, можно избавиться от рандома. Например, застабив Kernel#rand:

allow(Kernel).to receive(:rand).and_return(0.25)

banner = described_class.new(probability: 0.7)

expect(banner).to be_visible

Или зафиксировав сид:

# Осторожно, магия!
# rand с таким сидом первым числом возвращает 0.37
srand(42)

banner = described_class.new(probability: 0.7)

expect(banner).to be_visible

Ещё по теме:

Лайфхак: процедура завершения работы

Украл лайфхак, чтобы снизить тревогу после работы. Ну вы знаете: рабочий день давно закончился, пора бы уснуть, а ты лежишь в кровати до 2-3 ночи и стрессуешь, бесконечно возвращаясь к работе и задачам. А вот надо было Мише вот так ответить. Блин, на что переходить с Яндекс.Почты? Или лучше заплатить им? Но мы не ведем переговоры с террористами! А когда надо счётчики на воду поверить? Как в билинге учесть, что подарков может быть несколько? А если ещё и одновременно? Зачем я вообще пошёл в программисты?

Чтобы избавиться от этого и нормально спать, я использую процедуру завершения работы:

1. Разбираю инбоксы — почту, телегу и inbox.txt — и распихиваю всё по задачам.

2. Смотрю список дел на сегодня. Всё, что не успел, переношу на завтра или выкидываю.

3. Смотрю список дел на завтра и остаток недели.

4. Разбираю незакрытые вопросы в голове: вытаскиваю в задачи или заметку с идеями «на подумать».

5. Говорю себе: «Теперь питание компьютера можно отключить».

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

Мне помогло, попробуйте и вы.

RSpec: to be_within().of()

Как вы прекрасно знаете, в комплуктерах 0,1 + 0,2 ≠ 0,3. Аналогичные проблемы есть и в любых других вычислениях, связанных с дробями (считай, с числами с плавающей запятой). Поэтому в тестах никогда нельзя напрямую сравнивать дробные результаты вычислений.

Используйте be_within(delta).of(expected) — матчер, проверяющий, что полученное число находится в окрестности радиусом delta от нужного:

expect(score).to be_within(0.0001).of(2.19)

Аналогичная история и со временем: в Руби Time имеет точность до наносекунд, а БД — до микросекунд или секунд. В таких случаях приходится сравнивать приведением к юникстайму:

expect(deadline.to_i).to eq Time.new(2022, 7, 7).to_i

Или тем же матчером:

expect(deadline).to be_within(1.second).of(Time.new(2022, 7, 7))

И конечно, если работаете с деньгами, берите BigDecimal.

SPA на Реакте без Вебпака

В конце сентября у меня резко ухудшилось зрение в правом глазу: даже верхнюю строчку таблицы Сивцева видел с трудом. Врачи установили ложную близорукость, назначили лекарства, ограничение зрительных нагрузок (ха!) и гимнастику для глаз «по Аветисову». Конечно, ни одно из приложений с гимнастикой для глаз мне не понравилось, и я написал свое. Чтобы было интереснее, добавил ограничений: Реакт, одна страница и никакого Вебпака.

Оказалось, это ограничение решается довольно легко. Все можно сделать на клиенте:

1. Берем react и react-dom с CDN:

<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

2. Берем babel с CDN:

<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

3. Добавляем рутовый элемент, в который отрендерим приложение:

<div id="root"></div>

4. Добавляем <script> в конец body, который babel скомпилирует в рантайме:

<script type="text/babel">

4. Пишем приложение внутри этого скрипта:

const App = (<h1>Ola!</h1>)

ReactDOM.render(<App />, document.getElementById('root'))

5. Вы великолепны!

RSpec: skip и pending

Ситуация: пришли дизайнеры, говорят, СРОЧНО меняем механизм рекомендаций к статье. Вы меняете код, запускаете тесты, получаете кучу ошибок: рекомендации теперь другие, результаты не соответствуют действительности. Дизайнеры и маркетологи стоят у вас над душой и просят выкатить все НЕМЕДЛЕННО. Что делать с «битыми» тестами?

В RSpec есть пара тегов для «пропуска» битых тестов: skip и pending. Добавляем тег к context, describe или it и указываем причину:

it "returns posts with tags commonly used together", skip: "Broken in #45"

it "returns posts with tags commonly used together", pending: "Blocked by #46"

skip и pending отличаются по смыслу: skip отмечает тесты, которые пропускаем по какой-то причине; pending отмечает тесты, которые ждут какого-то события, чтобы заработать. Соответственно, и ведут себя по-разному: skip пропускается, а pending выполняется. Если тесты в pending пройдут, RSpec свалится с ошибкой:

1) returns posts with tags commonly used together FIXED
     Expected pending 'Blocked by #46' to fail. No error was raised.

Короче, если на собеседовании вас спросят, чем отличается skip от pending, смело отвечайте: pending может завалить тесты, а skip — нет.

SSRF

Недавно я узнал об интересном типе атак на веб-приложения: SSRF — server-side request forgery. Идея простая: если ваше приложение принимает ссылки от пользователей, что-то по ним скачивает и отображает, то злоумышленник может скормить ссылку, например, на приватный ресурс в локалке, и получить доступ к данным. Например, в AWS можно использовать http://169.254.169.254/latest/meta-data/, чтобы получить креденшиалы.

Соответственно, уязвимы все места, в которых приложение что-то загружает по ссылке. Если вы пользуетесь готовыми библиотеками, то, скорее всего, в них уже встроена защита от SSRF. Например, Carrierwave использует для этого ssrf_filter.

Чтобы погрузиться в потенциальные варианты атак с помощью SSRF, взгляните на исходники ssrf_filter: https://github.com/arkadiyt/ssrf_filter/blob/main/lib/ssrf_filter/ssrf_filter.rb

Тут всё: локальные айпишники, ipv6, левые URI, бесконечные редиректы, CRLF инъекции, ДНС и публичные адреса.

Range в ActiveRecord

Полюбил открытые и закрытые интервалы в запросах в ActiveRecord. Было:

Charge.where("created_at >= ?", 5.minutes.ago)

Subscription.where("valid_until <= ?", Time.now)

User.where("created_at >= ? and created_at <= ?", 2.weeks.ago, 1.week.ago)

Стало:

Charge.where(created_at: 5.minutes.ago..)

Subscription.where(valid_until: ..Time.now)

User.where(created_at: (2.weeks.ago..1.week.ago))