描述在 grunt 中实现错误采样

解读

“错误采样”在前端构建语境里,通常指在 Grunt 任务链中捕获异常并按比例/条件记录、上报或降级,而不是让整个构建直接失败
国内一线团队(阿里、腾讯、字节等)在落地“灰度构建”或“弱阻塞 CI”时,都会要求构建脚本具备“错误采样”能力:

  1. 不影响主干流程(构建不红);
  2. 把异常信息按采样率落到日志、飞书群、Sentry 等;
  3. 支持动态开关(配置中心或环境变量)。
    面试问这道题,想看候选人能否把“Grunt 事件流 + 异常捕获 + 采样算法 + 插件机制”串成闭环,而不是只会“grunt --force”。

知识点

  1. Grunt 事件模型:task、target、grunt.event.emit/on。
  2. 异常捕获层级
    • grunt.registerTask 里 this.async 的 try/catch;
    • grunt.util.spawn 子进程异常;
    • grunt.fail 命名空间(warn、fatal、error)。
  3. 采样算法:时间窗口 + 计数器 + 随机数,或基于 traceId 的一致性哈希采样
  4. 插件扩展:grunt.task.run 可注入“wrapper task”,在正式任务前后做 AOP。
  5. 国内日志通道:SLS、CLS、飞书机器人、钉钉自定义机器人,需走内网代理 + AK/SK 鉴权
  6. 配置热更新:通过 grunt.file.readJSON 监听 .sampler.json,实现运行时采样率调整

答案

分四步落地,代码可直接放进 Gruntfile.js,不依赖私有 npm,符合国内网络。

  1. 声明采样配置
// config/sampler.json
{
  "rate": 10,          // 10% 采样
  "whitelist": ["eslint", "webpack"],  // 这些任务 100% 上报
  "blacklist": ["clean"]               // 完全不上报
}
  1. 实现采样核心
// tasks/lib/error-sampler.js
const crypto = require('crypto');
let counter = 0;
exports.shouldSample = function(taskName) {
  const cfg = grunt.file.readJSON('config/sampler.json');
  if (cfg.whitelist.includes(taskName)) return true;
  if (cfg.blacklist.includes(taskName)) return false;
  // 一致性哈希采样,避免随机抖动
  const hash = crypto.createHash('md5').update(taskName + Date.now()).digest('hex');
  const bucket = parseInt(hash.slice(0, 4), 16) % 100;
  return bucket < cfg.rate;
};
  1. 包装所有任务
// tasks/safe-task.js
module.exports = function(grunt) {
  grunt.registerMultiTask('safe', 'wrap task with sampler', function() {
    const done = this.async();
    const taskName = this.data.realTask;
    try {
      grunt.task.run(taskName);
      done();
    } catch (e) {
      if (require('./lib/error-sampler').shouldSample(taskName)) {
        // 上报到飞书机器人
        grunt.util.spawn({
          cmd: 'curl',
          args: ['-X', 'POST', process.env.FEISHU_HOOK,
                 '-H', 'Content-Type: application/json',
                 '-d', JSON.stringify({msg_type: 'text', content: {text: `[Grunt] ${taskName} 异常: ${e.message}`}})]
        });
      }
      // 不阻断后续任务
      grunt.log.warn(`[Sampler] ${taskName} 异常已采样,构建继续`);
      done();
    }
  });
};
  1. 在 Gruntfile 中统一收口
module.exports = function(grunt) {
  grunt.loadTasks('tasks');
  // 把原始任务列表映射成 safe:xxx
  const rawTasks = ['eslint', 'webpack', 'clean'];
  const safeTasks = rawTasks.map(t => `safe:${t}`);
  grunt.registerTask('default', function() {
    grunt.option('force', true);          // 全局容错
    grunt.task.run(safeTasks);
  });
};

关键点

  • 用 grunt.option('force', true) 保证构建不红
  • 采样逻辑独立成 npm 私有包,可灰度;
  • 飞书/钉钉机器人地址走环境变量注入,避免泄漏;
  • 采样文件 config/sampler.json 被 grunt.file.watch 监听,热更新采样率无需重启 CI。

拓展思考

  1. 多 job 并发场景:Grunt 自身是单进程,但 Jenkins/GitLab Runner 会并发跑多个 job。此时可把采样结果写到共享 Redis(阿里云 Tair 内网版),用滑动窗口 + HyperLogLog 做去重,防止重复上报。
  2. 与 Monorepo 结合:在 pnpm/lerna 仓库里,每个子包有自己的 Gruntfile。可以在根目录提供统一采样 SDK,子包通过 grunt.task.loadNpmTasks('@company/grunt-sampler') 一键接入,采样配置走配置中心 ACM/Nacos,实现全链路错误采样治理
  3. 采样率动态调权:结合代码变更范围(git diff)与历史错误率,用函数计算实时计算“风险分”,高危险任务自动把采样率提到 100%,低危任务降到 1%,实现智能化错误采样