我们有一个日期错误每年发生两次,我们不知道,您可能也有ð±±±±
#javascript #网络开发人员 #开源 #bug

tl; dr

Novu的团队遇到了一个重大的错误,影响了CI/CD管道中的日期计算,从而阻碍了所有部署。

问题来自日期fns库的addmonths和submonths函数。

我们通过使用Adddays和Subdays功能修复了此问题。

Panic Gif


Novu:开源通知基础设施ð

只是关于我们的快速背景。 Novu是一个开源通知基础架构。我们基本上有助于管理所有产品通知。它可以是应用程序内的(像开发社区中的铃铛图标一样 - 网站cock链),电子邮件,短信等。

Novu Request Stars On Github


心态

在软件开发中工作时,我们始终为错误准备出现。

有时它们很小,易于识别,并且可以快速修复。

其他时候,他们就像我们今年的“年度虫子”的候选人。

这是一个如此难以捉摸和神秘的错误,以至于我们通过管道汇集了我们的代码库,并与日期操纵的复杂性面对面。

问题,不同的问题以及更多问题

我们的CI/CD管道失败。具体而言,两项阻止所有新部署的测试。是时候戴上我们的侦探帽子ðµï。

我们使用koude0进入了我们的承诺历史,但是它没有提供任何见解。 Git Bisect带我们回到承诺,在过去的6个月中,早在我们对系统的任何最新更改之前就很久了。这个错误是在Novu的开始时创建的吗?

但是,我们确实有一个线索。我们的单位测试失败表明我们的日期计算不正确。

收集线索

奇怪的是,差异只是一天。

const startDate = new Date("2023-08-31");
const oneMonthAhead = addMonths(startDate, 1);
const result = subMonths(oneMonthAhead, 1);
console.log(result);  // Expected: 31st of August, Reality: 30th of August

我们还发现这不是在7月31日发生的。

const startDate = new Date("2023-07-31");
const oneMonthAhead = addMonths(startDate, 1);
const result = subMonths(oneMonthAhead, 1);
console.log(result);  // Expected: 31st of July, Reality: 31th of July

但是该错误再次显示1月31日。

const startDate = new Date("2023-01-31");
const oneMonthAhead = addMonths(startDate, 1);
const result = subMonths(oneMonthAhead, 1);
console.log(result);  // Expected: 31st of January, Reality: 28th of January

因此,只有当我们增加1个月到一个月的时间比下个月更多的天数,然后减去1个月才能返回前一个月,才会发生此错误。

这是一个偷偷摸摸的

所以这是我们到目前为止所知道的:

  • 它只会显示在执行此特定逻辑顺序的系统上。
  • 该代码必须在所影响的少数日期之一上运行。
  • 在我们使用的任何库中的任何地方都不会记录这种效果。

最糟糕的是,此错误还显示了人力资源工具,金融工具,工资工具,公共政府工具都依赖此软件包,但不幸的是,它仍然比我们更加自我。

已经多次说,日期时间是编程中最棘手的方面之一,我们目前的困境是哈希的提醒。

This is tough GIF

为什么简单的动作会导致坏事

找到这个问题后,我们有了“尤里卡!”片刻。
我们的首席技术官Dima Grossman随后想到了在raycast上尝试它的想法。有趣的是,它也在他们的产品中发生。

Showing Raycast also uses date-fns

Mind Blown Gif

我们意识到这个问题源于本月的最后一天,但是到底发生了什么?

罪魁祸首:

date-fns icon

这个流行的公用事业库是该问题的核心。

具体来说,addMonthssubMonths功能。

addMonths函数在任何给定月的最后一天增加一个月时,将带您到下个月的最后一天。逻辑,对吗?

// source: https://github.com/date-fns/date-fns/blob/main/src/addMonths/index.ts
const daysInMonth = endOfDesiredMonth.getDate()
  if (dayOfMonth >= daysInMonth) {
    // If we're already at the end of the month, then this is the correct date
    // and we're done.
    return endOfDesiredMonth
  } else {
    // Otherwise, we now know that setting the original day-of-month value won't
    // cause an overflow, so set the desired day-of-month. Note that we can't
    // just set the date of `endOfDesiredMonth` because that object may have had
    // its time changed in the unusual case where where a DST transition was on
    // the last day of the month and its local time was in the hour skipped or
    // repeated next to a DST transition.  So we use `date` instead which is
    // guaranteed to still have the original time.
    _date.setFullYear(
      endOfDesiredMonth.getFullYear(),
      endOfDesiredMonth.getMonth(),
      dayOfMonth
    )
    return _date
  }

但是subMonths功能,而不是具有自己的专用逻辑,而只是将addMonths重复使用,为负数。 D.R.Y行动中的原则,但有意外的后果。

// source: https://github.com/date-fns/date-fns/blob/main/src/subMonths/index.ts
export default function subMonths<DateType extends Date>(
  date: DateType | number,
  amount: number
): DateType {
  return addMonths(date, -amount)
}

这是造成我们问题的原因

让我们这样说:

  • 在2月28日,加一个月,然后减去一个月,您将获得2月28日。那里没有问题。
  • ,但是,在8月31日,加一个月,然后减去一个月,然后在8月30日降落。那一天损失了日期的困境!

问题的核心是addMonths确定所需月末的方式。

几天不在月底,逻辑是合理的。

但是,在一个月的最后一天,该功能默认为下个月,而不是添加正确的天数。

简单的修复

为了确保一种始终如一的方法来操作,我们从使用addMonthssubMonths转移到addDayssubDays

Quicly Coding Cat Gif

这为处理日期计算提供了一种更精细,更精确的方法,重要的是,我们可以避开addMonths陷阱。

得到教训

此错误是一些关键领域的强大课程:

  1. 假设是有风险的:永远不要假设广泛使用的库是无误的。即使是最受欢迎的人也有怪癖。
  2. 测试是黄金:如果不是我们严格的测试套件,此错误可能仍然隐藏,只是在最不合时宜的时刻造成破坏。
  3. 日期很棘手:他们一直并将继续是软件开发的一个具有挑战性的方面。始终谨慎处理。

虽然这个错误在我们的管道上扔了一把扳手,但它也加强了全面测试的重要性以及不断提出质疑和挑战我们假设的必要性。

这个虫子的死亡

在一个代码的世界中,日期和时间构成了我们应用的关键部分,类似的错误不仅提供了打ic,而且提供了学习机会。下次您在应用程序中发现一个奇怪的问题时,请深入研究。谁知道,您可能只是发现下一个“年度错误”。

Bug Goodbye Gif

您可以在这里找到PR和问题: