refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {SentryConfig, SentryContext, SentryUser} from '@fluxer/sentry/src/SentryContracts';
import {DefaultSentryLogger} from '@fluxer/sentry/src/SentryLogger';
import {SentryNodeClient} from '@fluxer/sentry/src/SentryNodeClient';
import {SentryService} from '@fluxer/sentry/src/SentryService';
import type {SeverityLevel} from '@sentry/node';
const sentryService = new SentryService({
client: SentryNodeClient,
logger: DefaultSentryLogger,
});
export function initSentry(config?: SentryConfig): void {
sentryService.init(config);
}
export function captureException(error: Error, context?: SentryContext): void {
sentryService.captureException(error, context);
}
export function captureMessage(message: string, level: SeverityLevel = 'info', context?: SentryContext): void {
sentryService.captureMessage(message, level, context);
}
export async function flushSentry(timeout = 2000): Promise<void> {
await sentryService.flush(timeout);
}
export function setUser(user: SentryUser | null): void {
sentryService.setUser(user);
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {SentryBuildContext, SentryClientInitConfig, SentryConfig} from '@fluxer/sentry/src/SentryContracts';
const DEFAULT_ENVIRONMENT = 'production';
const DEFAULT_PRODUCTION_SAMPLE_RATE = 0.1;
const DEFAULT_NON_PRODUCTION_SAMPLE_RATE = 1.0;
export function resolveSentryInitConfig(config?: SentryConfig): SentryClientInitConfig | null {
const normalizedDsn = config?.dsn?.trim();
if (!normalizedDsn) {
return null;
}
const environment = resolveEnvironment(config?.environment);
const buildContext = resolveBuildContext(config);
return {
dsn: normalizedDsn,
environment,
...(config?.release !== undefined && {release: config.release}),
...(buildContext.number !== undefined && {dist: buildContext.number}),
...(config?.serviceName !== undefined && {serviceName: config.serviceName}),
sampleRate: resolveSampleRate(config?.sampleRate, environment),
buildContext,
};
}
function resolveEnvironment(environment?: string): string {
const trimmedEnvironment = environment?.trim();
if (trimmedEnvironment && trimmedEnvironment.length > 0) {
return trimmedEnvironment;
}
return DEFAULT_ENVIRONMENT;
}
function resolveSampleRate(sampleRate: number | undefined, environment: string): number {
if (isValidSampleRate(sampleRate)) {
return sampleRate;
}
if (environment === DEFAULT_ENVIRONMENT) {
return DEFAULT_PRODUCTION_SAMPLE_RATE;
}
return DEFAULT_NON_PRODUCTION_SAMPLE_RATE;
}
function isValidSampleRate(sampleRate: number | undefined): sampleRate is number {
return typeof sampleRate === 'number' && Number.isFinite(sampleRate) && sampleRate >= 0 && sampleRate <= 1;
}
function resolveBuildContext(config?: SentryConfig): SentryBuildContext {
return {
...(config?.buildSha !== undefined && {sha: config.buildSha}),
...(config?.buildNumber !== undefined && {number: config.buildNumber}),
...(config?.buildTimestamp !== undefined && {timestamp: config.buildTimestamp}),
...(config?.releaseChannel !== undefined && {channel: config.releaseChannel}),
};
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {SeverityLevel} from '@sentry/node';
export interface SentryConfig {
dsn?: string;
environment?: string;
release?: string;
serviceName?: string;
sampleRate?: number;
buildSha?: string;
buildNumber?: string;
buildTimestamp?: string;
releaseChannel?: string;
}
export interface SentryContext {
[key: string]: unknown;
}
export interface SentryUser {
id?: string;
username?: string;
email?: string;
ip_address?: string;
}
export interface SentryBuildContext {
sha?: string;
number?: string;
timestamp?: string;
channel?: string;
}
export interface SentryClientInitConfig {
dsn: string;
environment: string;
release?: string;
dist?: string;
serviceName?: string;
sampleRate: number;
buildContext: SentryBuildContext;
}
export interface SentryInitLogger {
info(msg: string): void;
info(obj: Record<string, unknown>, msg: string): void;
}
export interface ISentryClient {
init(config: SentryClientInitConfig): void;
captureException(error: Error, context?: SentryContext): void;
captureMessage(message: string, level: SeverityLevel, context?: SentryContext): void;
flush(timeout: number): Promise<void>;
setUser(user: SentryUser | null): void;
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {SentryInitLogger} from '@fluxer/sentry/src/SentryContracts';
export const DefaultSentryLogger: SentryInitLogger = {
info(objOrMsg: Record<string, unknown> | string, msg?: string): void {
if (typeof objOrMsg === 'string') {
process.stdout.write(`[sentry] ${objOrMsg}\n`);
return;
}
if (msg) {
process.stdout.write(`[sentry] ${msg} ${JSON.stringify(objOrMsg)}\n`);
}
},
};

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {
ISentryClient,
SentryBuildContext,
SentryClientInitConfig,
SentryContext,
SentryUser,
} from '@fluxer/sentry/src/SentryContracts';
import * as Sentry from '@sentry/node';
export const SentryNodeClient: ISentryClient = {
init(config: SentryClientInitConfig): void {
const buildContextEntries = toBuildContextEntries(config.buildContext);
Sentry.init({
dsn: config.dsn,
...(config.release !== undefined && {release: config.release}),
...(config.dist !== undefined && {dist: config.dist}),
environment: config.environment,
tracesSampleRate: config.sampleRate,
profilesSampleRate: config.sampleRate,
initialScope: (scope) => {
if (config.serviceName) {
scope.setTag('service', config.serviceName);
}
if (config.buildContext.channel) {
scope.setTag('release_channel', config.buildContext.channel);
}
if (config.buildContext.sha) {
scope.setTag('build_sha', config.buildContext.sha);
}
if (config.buildContext.number) {
scope.setTag('build_number', config.buildContext.number);
}
if (config.buildContext.timestamp) {
scope.setTag('build_timestamp', config.buildContext.timestamp);
}
if (buildContextEntries.length > 0) {
scope.setContext('build', Object.fromEntries(buildContextEntries));
}
return scope;
},
});
},
captureException(error: Error, context?: SentryContext): void {
Sentry.captureException(error, context !== undefined ? {extra: context} : undefined);
},
captureMessage(message: string, level: Sentry.SeverityLevel, context?: SentryContext): void {
Sentry.captureMessage(message, {
level,
...(context !== undefined && {extra: context}),
});
},
async flush(timeout: number): Promise<void> {
await Sentry.flush(timeout);
},
setUser(user: SentryUser | null): void {
Sentry.setUser(user);
},
};
function toBuildContextEntries(buildContext: SentryBuildContext): Array<[string, string]> {
const entries: Array<[string, string]> = [];
if (buildContext.sha) {
entries.push(['sha', buildContext.sha]);
}
if (buildContext.number) {
entries.push(['number', buildContext.number]);
}
if (buildContext.timestamp) {
entries.push(['timestamp', buildContext.timestamp]);
}
if (buildContext.channel) {
entries.push(['channel', buildContext.channel]);
}
return entries;
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {resolveSentryInitConfig} from '@fluxer/sentry/src/SentryConfig';
import type {
ISentryClient,
SentryConfig,
SentryContext,
SentryInitLogger,
SentryUser,
} from '@fluxer/sentry/src/SentryContracts';
import {DefaultSentryLogger} from '@fluxer/sentry/src/SentryLogger';
import type {SeverityLevel} from '@sentry/node';
export interface SentryServiceDependencies {
client: ISentryClient;
logger?: SentryInitLogger;
}
export class SentryService {
private readonly client: ISentryClient;
private readonly logger: SentryInitLogger;
private initialized = false;
public constructor(dependencies: SentryServiceDependencies) {
this.client = dependencies.client;
this.logger = dependencies.logger ?? DefaultSentryLogger;
}
public init(config?: SentryConfig): void {
if (this.initialized) {
this.logger.info('Sentry already initialized');
return;
}
const resolvedConfig = resolveSentryInitConfig(config);
if (resolvedConfig === null) {
this.logger.info('Sentry DSN not configured, skipping initialization');
return;
}
this.logger.info(
{
release: resolvedConfig.release,
environment: resolvedConfig.environment,
serviceName: resolvedConfig.serviceName,
},
'Initializing Sentry',
);
this.client.init(resolvedConfig);
this.initialized = true;
this.logger.info('Sentry initialized successfully');
}
public captureException(error: Error, context?: SentryContext): void {
if (!this.initialized) {
return;
}
this.client.captureException(error, context);
}
public captureMessage(message: string, level: SeverityLevel = 'info', context?: SentryContext): void {
if (!this.initialized) {
return;
}
this.client.captureMessage(message, level, context);
}
public async flush(timeout = 2000): Promise<void> {
if (!this.initialized) {
return;
}
await this.client.flush(timeout);
}
public setUser(user: SentryUser | null): void {
this.client.setUser(user);
}
}