From d4dd02c744ff43f240adfb0a537aa977f6baef6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:52:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Sentry=E3=83=90=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=81=AE=E8=87=AA=E5=8B=95=E8=A8=88?= =?UTF-8?q?=E8=A3=85=E3=82=92=E5=80=8B=E5=88=A5=E3=81=A7=E7=84=A1=E5=8A=B9?= =?UTF-8?q?=E5=8C=96=E5=87=BA=E6=9D=A5=E3=82=8B=E4=BB=95=E7=B5=84=E3=81=BF?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B=20(#17673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Sentryバックエンドの自動計装を個別で無効化出来る仕組みを追加する * fix comment --- .config/example.yml | 12 ++++ CHANGELOG.md | 1 + packages/backend/src/config.ts | 10 +++- .../adapters/SentryTelemetryAdapter.ts | 38 ++++++++++-- .../test/unit/SentryTelemetryAdapter.ts | 60 +++++++++++++++++++ 5 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 packages/backend/test/unit/SentryTelemetryAdapter.ts diff --git a/.config/example.yml b/.config/example.yml index 0aa8940677..f3f4985b25 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -303,6 +303,18 @@ id: 'aidx' #sentryForBackend: # enableNodeProfiling: true +# # Specify Sentry integration names to disable individual auto-instrumentation. +# # The names are integration .name values, not factory function names. +# # To check enabled names, set `options.debug: true` and see +# # "Integration installed: " in the Sentry logs. +# # +# # As of 2026-07-05 / @sentry/node 10.59.0, useful names for Misskey include: +# # Postgres ... DB queries (when pg is externalized) +# # Redis ... ioredis commands +# # Fastify ... inbound HTTP routes +# # Http ... inbound/outbound HTTP; disabling this can also affect request isolation +# # NodeFetch ... fetch/undici requests +# disabledIntegrations: ['Postgres'] # options: # dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c00cce61..61b57d0d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ ### Server - Enhance: センシティブメディアの判定を外部サービス ([sensitive-detector](https://github.com/misskey-dev/sensitive-detector)) に分離し、`nsfwjs` / `@tensorflow/tfjs(-node)` の同梱と NSFW 判定モデルを廃止 (#16804) +- Enhance: Sentry バックエンドの自動計装を `sentryForBackend.disabledIntegrations` で個別に無効化できるように - Enhance: Node.js 22.23.0以降、24.17.0以降、26.4.0以降をサポートするように - Enhance: Docker Image の Node.js を 26.4.0 に、Debian を trixie (v13) に更新 - Fix: `/stats` API のレスポンス型が正しくない問題を修正 diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index d67fe6fc6f..a9ce40db09 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -20,6 +20,12 @@ type RedisOptionsSource = Partial & { prefix?: string; }; +type SentryBackendConfig = { + options: Partial; + enableNodeProfiling: boolean; + disabledIntegrations?: string[]; +}; + /** * 設定ファイルの型 */ @@ -64,7 +70,7 @@ type Source = { index: string; scope?: 'local' | 'global' | string[]; }; - sentryForBackend?: { options: Partial; enableNodeProfiling: boolean; }; + sentryForBackend?: SentryBackendConfig; sentryForFrontend?: { options: Partial & { dsn: string }; vueIntegration?: SentryVue.VueIntegrationOptions | null; @@ -201,7 +207,7 @@ export type Config = { redisForJobQueue: RedisOptions & RedisOptionsSource; redisForTimelines: RedisOptions & RedisOptionsSource; redisForReactions: RedisOptions & RedisOptionsSource; - sentryForBackend: { options: Partial; enableNodeProfiling: boolean; } | undefined; + sentryForBackend: SentryBackendConfig | undefined; sentryForFrontend: { options: Partial & { dsn: string }; vueIntegration?: SentryVue.VueIntegrationOptions | null; diff --git a/packages/backend/src/core/telemetry/adapters/SentryTelemetryAdapter.ts b/packages/backend/src/core/telemetry/adapters/SentryTelemetryAdapter.ts index c155c9d74c..8b18b5b5e7 100644 --- a/packages/backend/src/core/telemetry/adapters/SentryTelemetryAdapter.ts +++ b/packages/backend/src/core/telemetry/adapters/SentryTelemetryAdapter.ts @@ -6,6 +6,34 @@ import type { Config } from '@/config.js'; import type { TelemetryAdapter, TelemetryCaptureMessageOptions } from './TelemetryAdapter.js'; +type SentryIntegrationsOption = NonNullable; +type SentryIntegrationFactory = Extract any[]>; +type SentryIntegration = Parameters[0][number]; + +type BuildSentryIntegrationsOptions = { + disabledIntegrations?: string[]; + enableNodeProfiling: boolean; + nodeProfilingIntegration?: () => SentryIntegration; + warn?: (message: string) => void; +}; + +export function buildSentryIntegrations(options: BuildSentryIntegrationsOptions): SentryIntegrationFactory { + return (defaults) => { + const disabledIntegrations = new Set(options.disabledIntegrations ?? []); + const defaultIntegrationNames = new Set(defaults.map((integration) => integration.name)); + const unknownIntegrations = [...disabledIntegrations].filter((name) => !defaultIntegrationNames.has(name)); + + if (unknownIntegrations.length > 0) { + (options.warn ?? console.warn)(`Unknown Sentry integration configured in sentryForBackend.disabledIntegrations: ${unknownIntegrations.join(', ')}`); + } + + return [ + ...defaults.filter((integration) => !disabledIntegrations.has(integration.name)), + ...(options.enableNodeProfiling && options.nodeProfilingIntegration != null ? [options.nodeProfilingIntegration()] : []), + ]; + }; +} + export class SentryTelemetryAdapter implements TelemetryAdapter { private constructor( private readonly Sentry: typeof import('@sentry/node'), @@ -17,10 +45,6 @@ export class SentryTelemetryAdapter implements TelemetryAdapter { const { nodeProfilingIntegration } = await import('@sentry/profiling-node'); Sentry.init({ - integrations: [ - ...(config.enableNodeProfiling ? [nodeProfilingIntegration()] : []), - ], - // Performance Monitoring tracesSampleRate: 1.0, // Capture 100% of the transactions @@ -30,6 +54,12 @@ export class SentryTelemetryAdapter implements TelemetryAdapter { maxBreadcrumbs: 0, ...config.options, + + integrations: buildSentryIntegrations({ + disabledIntegrations: config.disabledIntegrations, + enableNodeProfiling: config.enableNodeProfiling, + nodeProfilingIntegration, + }), }); return new SentryTelemetryAdapter(Sentry); diff --git a/packages/backend/test/unit/SentryTelemetryAdapter.ts b/packages/backend/test/unit/SentryTelemetryAdapter.ts new file mode 100644 index 0000000000..4e66fc08e7 --- /dev/null +++ b/packages/backend/test/unit/SentryTelemetryAdapter.ts @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test, vi } from 'vitest'; +import { buildSentryIntegrations } from '@/core/telemetry/adapters/SentryTelemetryAdapter.js'; + +type TestIntegration = Parameters>[0][number]; + +function testIntegration(name: string): TestIntegration { + return { name }; +} + +describe('SentryTelemetryAdapter', () => { + test('removes disabled integrations from Sentry defaults', () => { + const integrations = buildSentryIntegrations({ + disabledIntegrations: ['Postgres'], + enableNodeProfiling: false, + }); + + const result = integrations([ + testIntegration('Http'), + testIntegration('Postgres'), + testIntegration('Redis'), + ]); + + expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http', 'Redis']); + }); + + test('keeps profiling integration when enabled', () => { + const integrations = buildSentryIntegrations({ + disabledIntegrations: [], + enableNodeProfiling: true, + nodeProfilingIntegration: () => testIntegration('ProfilingIntegration'), + }); + + const result = integrations([ + testIntegration('Http'), + ]); + + expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http', 'ProfilingIntegration']); + }); + + test('warns about unknown disabled integration names without removing defaults', () => { + const warn = vi.fn(); + const integrations = buildSentryIntegrations({ + disabledIntegrations: ['Unknown'], + enableNodeProfiling: false, + warn, + }); + + const result = integrations([ + testIntegration('Http'), + ]); + + expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http']); + expect(warn).toHaveBeenCalledWith('Unknown Sentry integration configured in sentryForBackend.disabledIntegrations: Unknown'); + }); +});