@ 剪贴板- 健康/ 通知
通过第三方供应商发送通知.
目录
使用量
triggerChunked
triggerChunked 将全部、不可改变的触发请求保存在工作有序时间,消除有呆滞数据的问题,将请求排入提供者限制范围,以及在工作执行时间更新请求时可能出现的一能关键冲突.
-
搜索您的服务
triggerNotification.constants.ts, (中文(简体) ),triggerNotification.job.ts财务报告和审定财务报表notificationClient.provider.ts如果它们不存在,则创建它们:// triggerNotification.constants.ts export const TRIGGER_NOTIFICATION_JOB_NAME = "TriggerNotificationJob";// triggerNotification.job.ts import type { BaseHandler } from "@clipboard-health/background-jobs-adapter"; import { ERROR_CODES, isClientError, type NotificationClient, type SerializableTriggerChunkedRequest, toTriggerChunkedRequest, } from "@clipboard-health/notifications"; import { isFailure } from "@clipboard-health/util-ts"; import { CBHLogger } from "./setup"; import { TRIGGER_NOTIFICATION_JOB_NAME } from "./triggerNotification.constants"; /** * For @clipboard-health/mongo-jobs: * 1. Implement `HandlerInterface<SerializableTriggerChunkedRequest>`. * 2. The 10 default `maxAttempts` with exponential backoff of `2^attemptsCount` means ~17 minutes * of cumulative delay. If your notification could be stale before this, set * `SerializableTriggerChunkedRequest.expiresAt` when enqueueing. * * For @clipboard-health/background-jobs-postgres: * 1. Implement `Handler<SerializableTriggerChunkedRequest>`. * 2. The 20 default `maxRetryAttempts` with exponential backoff of `10s * 2^(attempt - 1)` means * ~121 days of cumulative delay. If your notification could be stale before this, set * `maxRetryAttempts` (and `SerializableTriggerChunkedRequest.expiresAt`) when enqueueing. */ export class TriggerNotificationJob implements BaseHandler<SerializableTriggerChunkedRequest> { // For background-jobs-postgres, use `public static queueName = TRIGGER_NOTIFICATION_JOB_NAME;` public name = TRIGGER_NOTIFICATION_JOB_NAME; private readonly logger = new CBHLogger({ defaultMeta: { logContext: TRIGGER_NOTIFICATION_JOB_NAME, }, }); public constructor( // @Inject(NOTIFICATION_CLIENT_TOKEN) private readonly client: NotificationClient, ) {} public async perform( data: SerializableTriggerChunkedRequest, /** * For mongo-jobs, implement `BackgroundJobType<SerializableTriggerChunkedRequest>`, which has * `_id`, `attemptsCount`, and `uniqueKey`. * * For background-jobs-postgres, implement `Job<SerializableTriggerChunkedRequest>`, which has * `id`, `retryAttempts`, and `idempotencyKey`. */ job: { _id: string; attemptsCount: number; uniqueKey?: string }, ) { const metadata = { // For background-jobs-postgres, this is called `retryAttempts`. attempt: job.attemptsCount + 1, jobId: job._id, recipientCount: data.body.recipients.length, workflowKey: data.workflowKey, }; this.logger.info("TriggerNotificationJob processing", metadata); try { const request = toTriggerChunkedRequest(data, { attempt: metadata.attempt, idempotencyKey: job.uniqueKey ?? metadata.jobId, // In case the tests are moving the time forward we need to ensure notifications don't expire. // ...(isTestMode && { expiresAt: new Date(3000, 0, 1) }), }); const result = await this.client.triggerChunked(request); if (isFailure(result)) { const code = result.error.issues[0]?.code; // Expired: the notification is no longer relevant (e.g. a reminder for an event that // has already started). Not actionable, so log at WARN and move on. if (code === ERROR_CODES.expired) { this.logger.warn("TriggerNotificationJob skipped due to expiry", { ...metadata }); return; } // The request itself is broken (empty/over-max recipients, invalid expiresAt or // idempotency key, missing signing key, or a non-429 4xx from the provider). // Retrying won't help — log at ERROR so it pages and we can fix the caller. if (isClientError(code)) { this.logger.error("TriggerNotificationJob skipped due to client error", { ...metadata, error: result.error, }); return; } // Everything else (rateLimited, unknown, 5xx, network) is transient — throw so the // job retries with backoff. throw result.error; } const success = "TriggerNotificationJob success"; this.logger.info(success, { ...metadata, response: result.value }); // For background-jobs-postgres, return the `success` string result. } catch (error) { this.logger.error("TriggerNotificationJob failure", { ...metadata, error }); throw error; } } }import { NotificationClient } from "@clipboard-health/notifications"; import { CBHLogger, type Provider, toLogger, tracer } from "./setup"; export const NOTIFICATION_CLIENT_TOKEN = "NOTIFICATION_CLIENT"; export const NotificationClientProvider: Provider<NotificationClient> = { provide: NOTIFICATION_CLIENT_TOKEN, useFactory: (): NotificationClient => new NotificationClient({ apiKey: process.env["KNOCK_API_KEY"]!, logger: toLogger( new CBHLogger({ defaultMeta: { context: "NotificationClient" }, }), ), tracer, }), }; -
搜索服务中存储工作流程密钥的常数 。 如果没有,就创造它:
/* eslint sort-keys: "error" */ /** * Alphabetical list of workflow keys. */ export const WORKFLOW_KEYS = { eventStartingReminder: "event-starting-reminder", } as const; -
构建您的
SerializableTriggerChunkedRequest并调整你的工作。 想着排队TriggerNotificationJob作为一项职能,要求以最佳做法的方式发送通知。 你不应该打电话triggerChunked直接说 例如,如果您的通知被延迟, 创建一个未来运行的背景任务, 做任何必要的检查以确保您应该通知, 然后排队TriggerNotificationJob。 。 。 .import type { BackgroundJobsAdapter } from "@clipboard-health/background-jobs-adapter"; import type { SerializableTriggerChunkedRequest } from "@clipboard-health/notifications"; import { BackgroundJobsService } from "./setup"; import { TRIGGER_NOTIFICATION_JOB_NAME } from "./triggerNotification.constants"; import { WORKFLOW_KEYS } from "./workflowKeys"; /** * Enqueue a notification job in the same database transaction as the changes it's notifying about. * The `session` option is called `transaction` in `background-jobs-postgres`. */ async function enqueueTriggerNotificationJob(adapter: BackgroundJobsAdapter) { // Assume this comes from a database and are used as template variables... const notificationData = { favoriteColor: "blue", // Use @clipboard-health/date-time's formatShortDateTime in your service for consistency. favoriteAt: new Date().toISOString(), secret: "2", }; const jobData: SerializableTriggerChunkedRequest = { // Important: Read the TypeDoc documentation for additional context. body: { recipients: ["userId-1", "userId-2"], data: notificationData, }, // Helpful when controlling notifications with feature flags. dryRun: false, // Set expiresAt at enqueue-time so it remains stable across job retries. Use date-fns in your // service instead of this manual calculation. expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(), // Keys to redact from logs keysToRedact: ["secret"], workflowKey: WORKFLOW_KEYS.eventStartingReminder, }; // Option 1 (default): Automatically use background job ID as idempotency key. await adapter.enqueue(TRIGGER_NOTIFICATION_JOB_NAME, jobData, { session: "..." }); // Option 2 (advanced): Provide custom idempotency key to job and notification libraries for more // control. You'd use this to provide enqueue-time deduplication. For example, if you enqueue when // a user clicks a button and only want them to receive one notification. await adapter.enqueue(TRIGGER_NOTIFICATION_JOB_NAME, jobData, { // Called `idempotencyKey` in `background-jobs-postgres`. unique: `meeting-123-reminder`, session: "...", }); } void enqueueTriggerNotificationJob( // Use your instance of `@clipboard-health/mongo-jobs` or `@clipboard-health/background-jobs-postgres` here. new BackgroundJobsService(), );
调试
Knock 的回复和错误的日志条目包括 knockRequestId敲门的身份证明.
过滤器由 请求编号 下级 观察性 > 翻译: 日志 内
敲打仪表板 看着Knock自己对请求和回应的看法
尸体 尸体 尸体 尸体 尸体 敲打其中的长值, 标记其下降 __knock_truncated__ 或者说
[TRUNCATED],并保留30天 API 日志。 。 。 .
地方发展指挥部
见 package.json scripts 用于命令列表.