summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2024-05-28 18:17:39 +0900
committerGitHub <noreply@github.com>2024-05-28 18:17:39 +0900
commite2eb7e8ca99839cedc01ef35c16f23da13b4ba3b (patch)
treea435d61a5fd731b5326112461f1a5243a624ad0a /packages/backend/src
parentBump version to 2024.5.0-beta.5 (diff)
parentfeat: sentry integration (#13897) (diff)
downloadsharkey-e2eb7e8ca99839cedc01ef35c16f23da13b4ba3b.tar.gz
sharkey-e2eb7e8ca99839cedc01ef35c16f23da13b4ba3b.tar.bz2
sharkey-e2eb7e8ca99839cedc01ef35c16f23da13b4ba3b.zip
Merge branch 'develop' into release/2024.5.0
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/boot/master.ts20
-rw-r--r--packages/backend/src/config.ts7
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts117
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts8
-rw-r--r--packages/backend/src/core/activitypub/type.ts1
-rw-r--r--packages/backend/src/queue/processors/InboxProcessorService.ts13
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts77
7 files changed, 163 insertions, 80 deletions
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 30f9477ccf..75e1a80cd1 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -10,6 +10,8 @@ import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
import Logger from '@/logger.js';
import { loadConfig } from '@/config.js';
import type { Config } from '@/config.js';
@@ -71,6 +73,24 @@ export async function masterMain() {
bootLogger.succ('Misskey initialized');
+ if (config.sentryForBackend) {
+ Sentry.init({
+ integrations: [
+ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+ ],
+
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
+ profilesSampleRate: 1.0,
+
+ maxBreadcrumbs: 0,
+
+ ...config.sentryForBackend.options,
+ });
+ }
+
if (envOption.disableClustering) {
if (envOption.onlyServer) {
await server();
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 0ca1fa55c1..0ac521d409 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -7,6 +7,7 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
+import * as Sentry from '@sentry/node';
import type { RedisOptions } from 'ioredis';
type RedisOptionsSource = Partial<RedisOptions> & {
@@ -56,6 +57,8 @@ type Source = {
index: string;
scope?: 'local' | 'global' | string[];
};
+ sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
+ sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
@@ -166,6 +169,8 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
+ sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
+ sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
@@ -234,6 +239,8 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
+ sentryForBackend: config.sentryForBackend,
+ sentryForFrontend: config.sentryForFrontend,
id: config.id,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 1621c41bcc..d0d206760c 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -28,6 +28,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserR
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -36,9 +37,8 @@ import { ApResolverService } from './ApResolverService.js';
import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Resolver } from './ApResolverService.js';
-import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
+import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
@Injectable()
export class ApInboxService {
@@ -90,13 +90,15 @@ export class ApInboxService {
}
@bindThis
- public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
+ public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
+ let result = undefined as string | void;
if (isCollectionOrOrderedCollection(activity)) {
+ const results = [] as [string, string | void][];
const resolver = this.apResolverService.createResolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
const act = await resolver.resolve(item);
try {
- await this.performOneActivity(actor, act);
+ results.push([getApId(item), await this.performOneActivity(actor, act)]);
} catch (err) {
if (err instanceof Error || typeof err === 'string') {
this.logger.error(err);
@@ -105,8 +107,13 @@ export class ApInboxService {
}
}
}
+
+ const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok')));
+ if (hasReason) {
+ result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
+ }
} else {
- await this.performOneActivity(actor, activity);
+ result = await this.performOneActivity(actor, activity);
}
// ついでにリモートユーザーの情報が古かったら更新しておく
@@ -117,42 +124,43 @@ export class ApInboxService {
});
}
}
+ return result;
}
@bindThis
- public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
+ public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
if (actor.isSuspended) return;
if (isCreate(activity)) {
- await this.create(actor, activity);
+ return await this.create(actor, activity);
} else if (isDelete(activity)) {
- await this.delete(actor, activity);
+ return await this.delete(actor, activity);
} else if (isUpdate(activity)) {
- await this.update(actor, activity);
+ return await this.update(actor, activity);
} else if (isFollow(activity)) {
- await this.follow(actor, activity);
+ return await this.follow(actor, activity);
} else if (isAccept(activity)) {
- await this.accept(actor, activity);
+ return await this.accept(actor, activity);
} else if (isReject(activity)) {
- await this.reject(actor, activity);
+ return await this.reject(actor, activity);
} else if (isAdd(activity)) {
- await this.add(actor, activity).catch(err => this.logger.error(err));
+ return await this.add(actor, activity);
} else if (isRemove(activity)) {
- await this.remove(actor, activity).catch(err => this.logger.error(err));
+ return await this.remove(actor, activity);
} else if (isAnnounce(activity)) {
- await this.announce(actor, activity);
+ return await this.announce(actor, activity);
} else if (isLike(activity)) {
- await this.like(actor, activity);
+ return await this.like(actor, activity);
} else if (isUndo(activity)) {
- await this.undo(actor, activity);
+ return await this.undo(actor, activity);
} else if (isBlock(activity)) {
- await this.block(actor, activity);
+ return await this.block(actor, activity);
} else if (isFlag(activity)) {
- await this.flag(actor, activity);
+ return await this.flag(actor, activity);
} else if (isMove(activity)) {
- await this.move(actor, activity);
+ return await this.move(actor, activity);
} else {
- this.logger.warn(`unrecognized activity type: ${activity.type}`);
+ return `unrecognized activity type: ${activity.type}`;
}
}
@@ -234,38 +242,49 @@ export class ApInboxService {
}
@bindThis
- private async add(actor: MiRemoteUser, activity: IAdd): Promise<void> {
+ private async add(actor: MiRemoteUser, activity: IAdd): Promise<string | void> {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
if (activity.target == null) {
- throw new Error('target is null');
+ return 'target is null';
}
if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
- if (note == null) throw new Error('note not found');
+ if (note == null) return 'note not found';
await this.notePiningService.addPinned(actor, note.id);
return;
}
- throw new Error(`unknown target: ${activity.target}`);
+ return `unknown target: ${activity.target}`;
}
@bindThis
- private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<void> {
+ private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<string | void> {
const uri = getApId(activity);
this.logger.info(`Announce: ${uri}`);
+ const resolver = this.apResolverService.createResolver();
+
+ if (!activity.object) return 'skip: activity has no object property';
const targetUri = getApId(activity.object);
+ if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
+
+ const target = await resolver.resolve(activity.object).catch(e => {
+ this.logger.error(`Resolution failed: ${e}`);
+ return e;
+ });
+
+ if (isPost(target)) return await this.announceNote(actor, activity, target);
- await this.announceNote(actor, activity, targetUri);
+ return `skip: unknown object type ${getApType(target)}`;
}
@bindThis
- private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
+ private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise<string | void> {
const uri = getApId(activity);
if (actor.isSuspended) {
@@ -288,24 +307,21 @@ export class ApInboxService {
// Announce対象をresolve
let renote;
try {
- renote = await this.apNoteService.resolveNote(targetUri);
- if (renote == null) throw new Error('announce target is null');
+ renote = await this.apNoteService.resolveNote(target);
+ if (renote == null) return 'announce target is null';
} catch (err) {
// 対象が4xxならスキップ
if (err instanceof StatusError) {
if (!err.isRetryable) {
- this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
- return;
+ return `Ignored announce target ${target.id} - ${err.statusCode}`;
}
-
- this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
+ return `Error in announce target ${target.id} - ${err.statusCode}`;
}
throw err;
}
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
- this.logger.warn('skip: invalid actor for this activity');
- return;
+ return 'skip: invalid actor for this activity';
}
this.logger.info(`Creating the (Re)Note: ${uri}`);
@@ -314,8 +330,7 @@ export class ApInboxService {
const createdAt = activity.published ? new Date(activity.published) : null;
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
- this.logger.warn('skip: malformed createdAt');
- return;
+ return 'skip: malformed createdAt';
}
await this.noteCreateService.create(actor, {
@@ -349,11 +364,15 @@ export class ApInboxService {
}
@bindThis
- private async create(actor: MiRemoteUser, activity: ICreate): Promise<void> {
+ private async create(actor: MiRemoteUser, activity: ICreate): Promise<string | void> {
const uri = getApId(activity);
this.logger.info(`Create: ${uri}`);
+ if (!activity.object) return 'skip: activity has no object property';
+ const targetUri = getApId(activity.object);
+ if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
+
// copy audiences between activity <=> object.
if (typeof activity.object === 'object') {
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
@@ -380,7 +399,7 @@ export class ApInboxService {
if (isPost(object)) {
await this.createNote(resolver, actor, object, false, activity);
} else {
- this.logger.warn(`Unknown type: ${getApType(object)}`);
+ return `Unknown type: ${getApType(object)}`;
}
}
@@ -422,7 +441,7 @@ export class ApInboxService {
@bindThis
private async delete(actor: MiRemoteUser, activity: IDelete): Promise<string> {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
// 削除対象objectのtype
@@ -581,29 +600,29 @@ export class ApInboxService {
}
@bindThis
- private async remove(actor: MiRemoteUser, activity: IRemove): Promise<void> {
+ private async remove(actor: MiRemoteUser, activity: IRemove): Promise<string | void> {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
if (activity.target == null) {
- throw new Error('target is null');
+ return 'target is null';
}
if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
- if (note == null) throw new Error('note not found');
+ if (note == null) return 'note not found';
await this.notePiningService.removePinned(actor, note.id);
return;
}
- throw new Error(`unknown target: ${activity.target}`);
+ return `unknown target: ${activity.target}`;
}
@bindThis
private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> {
if (actor.uri !== activity.actor) {
- throw new Error('invalid actor');
+ return 'invalid actor';
}
const uri = activity.id ?? activity;
@@ -614,7 +633,7 @@ export class ApInboxService {
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
- throw e;
+ return e;
});
// don't queue because the sender may attempt again when timeout
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 4e361b57bc..e6dff067f3 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -81,20 +81,20 @@ export class ApNoteService {
const expectHost = this.utilityService.extractDbHost(uri);
if (!validPost.includes(getApType(object))) {
- return new Error(`invalid Note: invalid object type ${getApType(object)}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
}
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
- return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
}
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
if (object.attributedTo && actualHost !== expectHost) {
- return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
}
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
- return new Error('invalid Note: published timestamp is malformed');
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
}
return null;
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 09322888d5..5b6c6c8ca6 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -328,3 +328,4 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
+export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index f465339075..fa7009f8f5 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -204,13 +204,22 @@ export class InboxProcessorService {
// アクティビティを処理
try {
- await this.apInboxService.performActivity(authUser.user, activity);
+ const result = await this.apInboxService.performActivity(authUser.user, activity);
+ if (result && !result.startsWith('ok')) {
+ this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`);
+ return result;
+ }
} catch (e) {
if (e instanceof IdentifiableError) {
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
return 'blocked notes with prohibited words';
}
- if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') return 'actor has been suspended';
+ if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') {
+ return 'actor has been suspended';
+ }
+ if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note
+ return e.message;
+ }
}
throw e;
}
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 9836689872..271ef80554 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
+import * as Sentry from '@sentry/node';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -17,6 +18,7 @@ import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import type { Config } from '@/config.js';
import { ApiError } from './error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
@@ -38,6 +40,9 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
@@ -88,6 +93,48 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
+ #onExecError(ep: IEndpoint, data: any, err: Error): void {
+ if (err instanceof ApiError || err instanceof AuthenticationError) {
+ throw err;
+ } else {
+ const errId = randomUUID();
+ this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ ep: ep.name,
+ ps: data,
+ e: {
+ message: err.message,
+ code: err.name,
+ stack: err.stack,
+ id: errId,
+ },
+ });
+ console.error(err, errId);
+
+ if (this.config.sentryForBackend) {
+ Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ extra: {
+ ep: ep.name,
+ ps: data,
+ e: {
+ message: err.message,
+ code: err.name,
+ stack: err.stack,
+ id: errId,
+ },
+ },
+ });
+ }
+
+ throw new ApiError(null, {
+ e: {
+ message: err.message,
+ code: err.name,
+ id: errId,
+ },
+ });
+ }
+ }
+
@bindThis
public handleRequest(
endpoint: IEndpoint & { exec: any },
@@ -362,31 +409,11 @@ export class ApiCallService implements OnApplicationShutdown {
}
// API invoking
- return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
- if (err instanceof ApiError || err instanceof AuthenticationError) {
- throw err;
- } else {
- const errId = randomUUID();
- this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
- ep: ep.name,
- ps: data,
- e: {
- message: err.message,
- code: err.name,
- stack: err.stack,
- id: errId,
- },
- });
- console.error(err, errId);
- throw new ApiError(null, {
- e: {
- message: err.message,
- code: err.name,
- id: errId,
- },
- });
- }
- });
+ if (this.config.sentryForBackend) {
+ return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)));
+ } else {
+ return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err));
+ }
}
@bindThis