@ddd-ts-kit/sentry

中文文档
0.106.0发布于 2026/11/01MITTypeScriptES Module构建来源已验证

向内核ErrorReporter端口后面的Sentry或GlitchTip报告错误:只有意外的失败,与跟踪相关,在发送之前清除。

中文使用文档
文档目录8 节
  1. 作文根
  2. 报道了什么
  3. 与OpenTelemetry
  4. 隐私
  5. 分组和标记
  6. 选项
  7. 开发小提示
  8. 在测试中

英语 · 法语

@ddd-ts-kit /哨兵

内核 ErrorReporter 在Sentry或GlitchTip上,它们使用相同的协议:一个适配器, 只有DSN不同。只有意想不到的失败,每一次失败都留下了痕迹 离开流程。中没有 domain/ 或者 application/ 导入此包或报告 处理程序抛出的任何东西,以及它们周围的东西都会报告。

npm i @ddd-ts-kit/sentry @sentry/node @opentelemetry/api

写对 @sentry/node 11:它所依赖的选项enableOpenTelemetrySetup, dataCollection)在以前的专业中不存在。

Where the Sentry reporter sits: handlers and aggregates throw and report nothing; five capture points around them, the command and query buses, the HTTP pipeline, the job scheduler, DeadLetterReports on the hooks of the event providers and reportProcessFailures on the process, hand an unexpected failure to the kernel ErrorReporter, once; the composition root binds the SentryErrorReporter of initSentry to it; the event is scrubbed, then passed to the beforeSend of the project, then sent to Sentry or GlitchTip; its trace id leads to the trace in the tracing backend

用例的代码永远不会看到报告器。它扔;它周围的东西决定它是否存在 失败是一个事件,用正在进行的工作和手的标识符来描述它 它变成了内核 ErrorReporter这个包实现的唯一端口。

作文根

// platform/observability/errors.ts: imported second, right after platform/observability/tracing.ts
import { reportProcessFailures } from '@ddd-ts-kit/kernel';
import { initSentry } from '@ddd-ts-kit/sentry';
import { loadConfig } from '@/platform/config/load-config';

// { dsn?, environment, release?, scrubMessages } of the validated configuration.
// Without a DSN nothing starts and nothing is reported.
export const errors = initSentry(loadConfig(process.env).errorReporting);

reportProcessFailures(errors); // uncaughtException and unhandledRejection: reported, then exit 1
// main.ts
import { tracing } from '@/platform/observability/tracing'; // first
import { errors } from '@/platform/observability/errors'; // second
import { DeadLetterReports, ExecutionScope } from '@ddd-ts-kit/kernel';
import { kernelModule } from '@ddd-ts-kit/di';
import { createApplication } from '@ddd-ts-kit/di/tsyringe';
import { reportHttpErrors } from '@ddd-ts-kit/http';
import { RabbitMqEventBus } from '@ddd-ts-kit/rabbitmq';

const deadLetters = new DeadLetterReports(errors);
const bus = new RabbitMqEventBus({ url, onFailure: (failure) => deadLetters.onFailure(failure) });

const app = createApplication({
  modules: [
    kernelModule({
      errorReporter: errors, // buses and jobs report through it, app.stop() flushes it
      eventBus: bus,
      eventSubscribers: { forContext: (name) => deadLetters.observing(bus.forConsumer(name)) },
    }),
    ...contexts,
  ],
});

const http = { onError: reportHttpErrors(errors), executionScope: app.resolve(ExecutionScope) };

process.on('SIGTERM', () => void app.stop().then(() => tracing.shutdown()));

它有三行字: initSentry, errorReporter: errors, onError: reportHttpErrors(errors). 流程处理程序和死信各占一行,供需要它们的用户使用。

init --errors sentry 在它的文件中精确地写入这些片段: platform/observability/errors.ts 作为 以上, SENTRY_DSN, SENTRY_ENVIRONMENT (默认值: NODE_ENV), SENTRY_RELEASE 和 SENTRY_SCRUB_MESSAGES 读到 platform/config/load-config.ts; errors 交的 main.ts 来 composeApplication,它的值是 kernelModule({ errorReporter }); reportHttpErrors 在 platform/auth/authentication/http-boundary.ts; DeadLetterReports 在 platform/events/create-event-provider.ts (经纪人)进 platform/events/start-outbox-relay.ts (被宣布死亡的出版物)。

加载顺序。 platform/observability/tracing.ts 保留第一个导入:OpenTelemetry补丁 http 和 驱动程序加载时,所以它必须在它们之前运行。 initSentry 没有补丁,所以 它的位置对于跟踪没有改变,无论两个顺序中哪个先开始(两个顺序都是) 测试)。它是第二个,所以当应用程序的其余部分加载时已经出现了故障 报道了 reportProcessFailures.

报道了什么

只是没人计划的事。一个 DomainError 是生意上的拒绝,是回答,而不是事件。

捕捉点附加的报告不报告
命令和查询总线kernelModule (默认的公共汽车运载 errorReportingMiddleware)命令或查询失败,但没有 DomainError一个 DomainError;事件被消耗时的任何东西
HTTPonError: reportHttpErrors(errors)每个请求都得到了5xx, a DomainError 映射到一个5xx包含4 xx
安排工作kernelModule (默认调度器观察 reportFailedRuns)赛跑的结果是 failed,包括超时一个 DomainError跳过或错过的事件
集成事件DeadLetterReports 在 onFailure 提供者的钩子事件放弃:重试已用尽,有毒消息,发件箱中继的发布被宣布为死亡传递失败并重新尝试
过程reportProcessFailures(errors)uncaughtException 和 unhandledRejection,因为 fatal

一次失败,一次报告。 看到错误对象的第一个捕获点报告它和 标志着它;它越过的边界后来承认它,也被称为 cause 一个错误 就这样吧。因此,在请求中失败的命令将由总线报告一次, 在命令的名称下,回答500的路由保持沉默。报告仍然 说明了它的来源: origin 是执行上下文的起源(http, schedule), 关联id和跟踪会指向请求。

One failure, one report: a request reaches the route, which sends a command on the bus; the handler throws an unexpected error; errorReportingMiddleware of the bus reports it under the name of the command and marks it, then lets it go on; the route answers 500 and reportHttpErrors recognises the marked error and stays silent; the tracker receives one event, with origin http, the correlation id and the trace id

事件。 消费者被重新尝试:它的第一次失败是预料之中的,报告它们也是预料之中的 掩盖真实的事件。因此,总线中间件在使用事件时什么也不说, 和 DeadLetterReports 一旦其提供者放弃该事件,就报告该事件。鱼钩 提供者只说代码,所以 observing(subscriber) 记住每个处理程序抛出了什么: 报告携带真正的错误及其堆栈。没有它,或者是一个有毒的信息,报告 是一个 DeadLetterError 命名代码。事件隐藏在 DomainError 被报道为 warning它还是输给了它的消费者。

An event whose handler keeps failing: at each attempt observing remembers what the handler threw and the provider calls onFailure; while the provider retries, onFailure reports nothing; at the last attempt the provider gives the event up, onFailure hears dead and reports once, with the error the handler really threw, origin event and the operation named after the event type and the consumer

同样的步骤,用手对内核的双子棋,作为对线路的测试:

import {
  DeadLetterReports,
  InMemoryErrorReporter,
  type IntegrationEvent,
  type IntegrationEventCallback,
  type IntegrationEventSubscriber,
} from '@ddd-ts-kit/kernel';

// A provider reduced to what matters here: it keeps the handler it is given.
const handlers = new Map<string, IntegrationEventCallback>();
const provider: IntegrationEventSubscriber = {
  subscribe: (type, handler) => {
    handlers.set(type, handler);

    return { unsubscribe: () => handlers.delete(type) };
  },
};

const reporter = new InMemoryErrorReporter();
const deadLetters = new DeadLetterReports(reporter);

deadLetters.observing(provider).subscribe('ordering.order.placed', async () => {
  throw new Error('ledger unavailable');
});

const event: IntegrationEvent = {
  id: 'evt-1',
  type: 'ordering.order.placed',
  version: 1,
  occurredAt: '2026-03-01T10:00:00.000Z',
  payload: {},
};
const deliver = handlers.get('ordering.order.placed');

// What the provider says of each failed delivery: codes and identifiers only.
const failure = { kind: 'handler', code: 'Error', eventId: 'evt-1', eventType: 'ordering.order.placed', consumer: 'billing' };

await deliver?.(event).catch(() => undefined); // attempt 1 fails: observing remembers the error
deadLetters.onFailure({ ...failure, attempt: 1 }); // retried: no report

await deliver?.(event).catch(() => undefined); // attempt 2, the last one, fails
deadLetters.onFailure({ ...failure, attempt: 2, dead: true }); // given up: one report

// One report: the Error('ledger unavailable') itself, origin 'event', operation 'ordering.order.placed billing'.
export const reports = reporter.reports;

在处理程序中。 没有什么要写的:处理程序抛出,它周围的捕获点报告。

import { DomainRefusal, type ErrorReporter } from '@ddd-ts-kit/kernel';

interface PaymentGateway {
  capture(paymentId: string): Promise<'captured' | 'declined'>;
}

interface CapturePaymentCommand {
  readonly paymentId: string;
}

// payments.codes.ts
export const PaymentsCodes = {
  PaymentDeclined: 'PAYMENT_DECLINED',
} as const;

// domain/errors/payment-declined.error.ts
export class PaymentDeclined extends DomainRefusal {
  readonly code = PaymentsCodes.PaymentDeclined;

  static forPayment(paymentId: string) {
    return PaymentDeclined.refusal(`Payment ${paymentId} was declined.`, { paymentId });
  }
}

// Instead of: a handler that reports by itself, and hides the failure from its caller.
export class CapturePaymentReportingItself {
  constructor(
    private readonly gateway: PaymentGateway,
    private readonly errors: ErrorReporter,
  ) {}

  async execute(command: CapturePaymentCommand): Promise<void> {
    try {
      await this.gateway.capture(command.paymentId);
    } catch (error) {
      this.errors.report(error, { origin: 'command', operation: 'CapturePayment', severity: 'error' });
    }
  }
}

// Write: a handler that throws. A refusal is a DomainError, answered and never reported;
// anything else goes up to the bus, which reports it once, with the actor, the tenant and the trace.
export class CapturePaymentHandler {
  constructor(private readonly gateway: PaymentGateway) {}

  async execute(command: CapturePaymentCommand): Promise<void> {
    if ((await this.gateway.capture(command.paymentId)) === 'declined') {
      throw PaymentDeclined.forPayment(command.paymentId);
    }
  }
}

与OpenTelemetry

@ddd-ts-kit/tracing 拥有跟踪程序提供程序、上下文管理器、传播程序和 auto-instrumentations。 initSentry 让他们独自一人:

  • enableOpenTelemetrySetup: false: SDK不注册任何跟踪程序提供程序和传播程序;
  • 没有SDK的默认集成,并且 enableRuntimeChannelInjection: false没有第二 仪器的 http 或指司机;
  • 否 tracesSampleRate 除非给定:否则不会将性能数据发送到跟踪器。

将两者联系起来的是:当报告失败时,报告程序获取跟踪id和 活动OpenTelemetry span的span id,并将它们放入 contexts.trace 事件的。 从Sentry或GlitchTip中的一个问题,跟踪id在跟踪后端打开跟踪。这个 跨度本身被导出一次,由工具包的出口商。

隐私

默认的选择在
请求从来没有收集。如果其他东西添加了一个:方法、不带查询字符串的URL、既不携带凭据也不携带地址的标头。没有尸体,就没有饼干一个项目 beforeSend
用户演员id,没有其他信息:没有电子邮件,没有姓名,没有地址
租户不发送tagTenant: true
局部变量从来没有
面包屑无自动记录:无话务台,无呼出Sentry.addBreadcrumb 项目决定的地方
异常消息发送后,失败语句引用的值被编校scrubMessages: true 为每个非 DomainError
为失败语句赋值引号永远不要在消息中说明错误的原因或集成复制的内容(extra(上下文):语句保留,包括占位符

消息。 消息是错误跟踪器中首先读取的内容,也是第一个读取的内容 个人数据泄露的地方:司机引用了他拒绝的那一行。默认保留它,因为 问题标题 [message removed] 浪费了所有的调查,因为追踪者是个 项目控制其访问和保留的内部工具。与 scrubMessages: true 只有 这个项目自己写的信息保存了下来,那些是它的 DomainError年代;输入,编码,堆栈 并为其他人留下痕迹。当跟踪器由第三方托管或 当数据足够敏感,操作人员不应该读取它时。的记录器 工具包(StdoutLogger, @ddd-ts-kit/pino, @ddd-ts-kit/winston)选择同一项下的选项 名字,同样的规则,同样的标记, SCRUBBED_MESSAGE 核的。

失败语句的值。 无论 scrubMessages 说, scrubEvent 替换的 [redacted] 驱动程序引用每个值,并保留语句,这是信号: Drizzle写后 params: 在一个 DrizzleQueryError;被侵犯的唯一的键 或外键约束(Key (email)=([redacted]) already exists.,在短信或邮件中 detail); 行不为空或检查约束被拒绝(Failing row contains ([redacted]).); PostgreSQL无法读取的值(invalid input syntax for type integer: "[redacted]"); Prisma对它拒绝的请求所提出的论据;一个名为 parameters, params 或者 bindings 这是对误差的积分。了解一下PostgreSQL的引号 $1, TypeORM是服务器的消息,其表单如上面所示。

beforeSend 在擦除的事件上最后运行,并可能通过回答而丢弃它 null.

DSN留在SDK客户端中。一个 SentryErrorReporter 这是记录,传播或 连载显示 SentryErrorReporter [client concealed];SDK的客户端,如果您登录 它自己显示了它的选项。

分组和标记

指纹从不包含消息,因此消息中的标识符不会解决一个问题 分成成千上万个问题 DomainError 是它的类和代码;任何其他错误都是 类、它的代码(如果有的话)以及它的第一帧的函数和文件(没有行) 数字。

标签: origin, operation, correlation_id, bounded_context, error_code, tenant_id 在请求。项目添加到的字段 ErrorReportExtensions 作为 report 上下文。

选项

选项默认的
dsn无没有它 initSentry 回答一个 NoopErrorReporter 什么都没有开始。 SENTRY_DSN 是不是读取后面的配置
environment, release无
enabledtruefalse 什么也没报告,不管深空网络是什么
sampleRate1发送的错误事件的共享
tracesSampleRate无仅适用于通过Sentry API启动的跨度;套件的跨度从来没有到过那里
scrubMessagesfalse看到“隐私”
tagTenantfalse
beforeSend无在试剂盒洗涤器之后运行
integrations无添加到链接错误和源上下文行

flush(timeoutMs) 答案 true 当没有东西可以送的时候。 app.stop() 用2来调用 秒; reportProcessFailures 退出前做。

开发小提示

GlitchTip 6作为PostgreSQL(14或更高版本)旁边的一个服务运行,并且由其推荐 文档,Valkey。开发映像引导用户、组织和项目 并打印DSN:

services:
  glitchtip:
    image: glitchtip/glitchtip:6.2.6
    profiles: [errors]
    depends_on:
      glitchtip-postgres: { condition: service_healthy }
      glitchtip-valkey: { condition: service_started }
    ports: ['127.0.0.1:8000:8000']
    environment:
      SERVER_ROLE: all_in_one # web and worker in one process
      DATABASE_URL: postgres://postgres:postgres@glitchtip-postgres:5432/glitchtip
      VALKEY_URL: redis://glitchtip-valkey:6379
      SECRET_KEY: local-development-only-not-a-secret # never in production: openssl rand -hex 32
      GLITCHTIP_DOMAIN: http://localhost:8000
      EMAIL_URL: consolemail://
      DEFAULT_FROM_EMAIL: glitchtip@localhost
      DEBUG: 'true' # development only: required by the bootstrap below
      GLITCHTIP_BOOTSTRAP_DEV: 'true' # user [email protected] / admin_pass, organization, project, DSN
    healthcheck:
      test: [CMD, python, healthcheck.py]
      interval: 10s
      timeout: 6s
      retries: 6
      start_period: 180s # the first start migrates the database
  glitchtip-postgres:
    image: postgres:18-alpine
    profiles: [errors]
    environment: { POSTGRES_DB: glitchtip, POSTGRES_PASSWORD: postgres }
    volumes: ['glitchtip-postgres:/var/lib/postgresql']
    healthcheck:
      test: [CMD-SHELL, pg_isready -U postgres -d glitchtip]
      interval: 5s
      retries: 12
  glitchtip-valkey:
    image: valkey/valkey:9-alpine
    profiles: [errors]

volumes:
  glitchtip-postgres:
docker compose --profile errors up -d --wait
docker compose logs glitchtip | grep 'DSN:'

把DSN放进去 SENTRY_DSN;从堆栈的另一个容器中替换 localhost 与 glitchtip. 使用上述用户登录http://localhost:8000。

在测试中

这个包没有任何内容:测试应用程序绑定 InMemoryErrorReporter, app.errors.reports 列出总线和作业报告的内容,作为数据:每个条目是 { error, report },抛出时的错误和 ErrorReport 就是这样。

版本与发布记录

@ddd-ts-kit/sentry 共发布 63 个版本,首次发布于 2026/10/11;过去 12 个月发布了 63 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.106.0latest2026/11/012 天前
0.105.02026/10/312 天前
0.104.02026/10/312 天前
0.103.02026/10/313 天前
0.102.02026/10/313 天前
0.101.02026/10/303 天前

关于 @ddd-ts-kit/sentry 的常见问题

@ddd-ts-kit/sentry 是什么?

向内核ErrorReporter端口后面的Sentry或GlitchTip报告错误:只有意外的失败,与跟踪相关,在发送之前清除。

如何安装 @ddd-ts-kit/sentry?

在项目目录运行 npm install @ddd-ts-kit/sentry,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@sentry/node(^11.6.0)、@opentelemetry/api(^1.9.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ddd-ts-kit/sentry 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ddd-ts-kit/sentry 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ddd-ts-kit/sentry 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ddd-ts-kit/sentry 可以商用吗?

@ddd-ts-kit/sentry 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ddd-ts-kit/sentry 还在维护吗?

最新版本 0.106.0 发布于 2026/11/01(2 天前);至今共发布 63 个版本,其中过去 12 个月发布了 63 个;首次发布于 2026/10/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。